pi-lens 4.1.2 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +1308 -0
  2. package/README.md +3 -0
  3. package/config/biome/core.jsonc +11 -2
  4. package/config/dependency-cruiser-eager-allowlist.json +91 -0
  5. package/dist/clients/actionable-warnings-logger.js +18 -3
  6. package/dist/clients/actionable-warnings.js +927 -80
  7. package/dist/clients/agent-behavior-client.js +13 -4
  8. package/dist/clients/ast-grep-client.js +136 -9
  9. package/dist/clients/ast-grep-rule-manager.js +60 -5
  10. package/dist/clients/ast-grep-tool-logger.js +2 -2
  11. package/dist/clients/availability-probe-flight.js +12 -0
  12. package/dist/clients/bash-file-access.js +1 -3
  13. package/dist/clients/biome-client.js +16 -2
  14. package/dist/clients/blocker-freshness.js +34 -0
  15. package/dist/clients/bootstrap.js +509 -73
  16. package/dist/clients/bounded-cache.js +152 -12
  17. package/dist/clients/bounded-telemetry.js +115 -23
  18. package/dist/clients/build-identity.js +95 -0
  19. package/dist/clients/bundled-resource-health.js +113 -0
  20. package/dist/clients/bus-events-logger.js +2 -2
  21. package/dist/clients/bus-publish.js +3 -0
  22. package/dist/clients/cache/rule-cache.js +33 -5
  23. package/dist/clients/cache-manager.js +105 -6
  24. package/dist/clients/cache-observability.js +46 -19
  25. package/dist/clients/cargo-manifest.js +422 -0
  26. package/dist/clients/cascade-logger.js +17 -3
  27. package/dist/clients/code-quality-warnings.js +13 -3
  28. package/dist/clients/complexity-client.js +16 -5
  29. package/dist/clients/config-core/deny.js +221 -0
  30. package/dist/clients/config-core/index.js +47 -0
  31. package/dist/clients/config-core/merge.js +357 -0
  32. package/dist/clients/config-core/normalize.js +340 -0
  33. package/dist/clients/config-core/process-spec.js +248 -0
  34. package/dist/clients/config-core/provenance.js +171 -0
  35. package/dist/clients/config-core/records.js +218 -0
  36. package/dist/clients/config-core/resolve.js +125 -0
  37. package/dist/clients/config-core/safe-object.js +78 -0
  38. package/dist/clients/config-core/schema.js +167 -0
  39. package/dist/clients/config-diagnostic-codes.js +281 -0
  40. package/dist/clients/config-locations.js +160 -0
  41. package/dist/clients/config-resolve.js +789 -0
  42. package/dist/clients/config-schema.js +197 -0
  43. package/dist/clients/config-warn.js +407 -0
  44. package/dist/clients/dead-code-client.js +33 -25
  45. package/dist/clients/dead-code-logger.js +2 -2
  46. package/dist/clients/deadline-utils.js +178 -0
  47. package/dist/clients/debug-handles.js +2 -2
  48. package/dist/clients/debug-heap.js +3 -3
  49. package/dist/clients/deferred-lsp-work.js +106 -0
  50. package/dist/clients/degradation-ledger.js +166 -8
  51. package/dist/clients/demoted-finding-render.js +14 -0
  52. package/dist/clients/dependency-checker.js +28 -22
  53. package/dist/clients/deps/minimatch.js +1 -1
  54. package/dist/clients/diagnostic-line-freshness.js +12 -9
  55. package/dist/clients/diagnostic-logger.js +14 -4
  56. package/dist/clients/dispatch/collect-later-tier.js +39 -0
  57. package/dist/clients/dispatch/dispatcher.js +304 -19
  58. package/dist/clients/dispatch/fact-store.js +328 -9
  59. package/dist/clients/dispatch/integration.js +214 -107
  60. package/dist/clients/dispatch/pending-runner-findings.js +87 -0
  61. package/dist/clients/dispatch/plan.js +21 -5
  62. package/dist/clients/dispatch/runners/actionlint.js +0 -1
  63. package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
  64. package/dist/clients/dispatch/runners/biome-check.js +0 -1
  65. package/dist/clients/dispatch/runners/cpp-check.js +0 -1
  66. package/dist/clients/dispatch/runners/credo.js +0 -1
  67. package/dist/clients/dispatch/runners/cue-vet.js +0 -1
  68. package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  69. package/dist/clients/dispatch/runners/detekt.js +0 -1
  70. package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  71. package/dist/clients/dispatch/runners/elixir-check.js +0 -1
  72. package/dist/clients/dispatch/runners/eslint.js +5 -2
  73. package/dist/clients/dispatch/runners/fact-rules.js +0 -1
  74. package/dist/clients/dispatch/runners/fish-indent.js +0 -1
  75. package/dist/clients/dispatch/runners/gleam-check.js +0 -1
  76. package/dist/clients/dispatch/runners/go-vet.js +1 -3
  77. package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  78. package/dist/clients/dispatch/runners/hadolint.js +0 -1
  79. package/dist/clients/dispatch/runners/helm-lint.js +16 -4
  80. package/dist/clients/dispatch/runners/helm-render.js +54 -10
  81. package/dist/clients/dispatch/runners/htmlhint.js +0 -1
  82. package/dist/clients/dispatch/runners/index.js +1 -3
  83. package/dist/clients/dispatch/runners/javac.js +0 -1
  84. package/dist/clients/dispatch/runners/ktlint.js +0 -1
  85. package/dist/clients/dispatch/runners/lsp.js +37 -2
  86. package/dist/clients/dispatch/runners/markdownlint.js +4 -2
  87. package/dist/clients/dispatch/runners/mypy.js +0 -1
  88. package/dist/clients/dispatch/runners/oxlint.js +24 -22
  89. package/dist/clients/dispatch/runners/php-lint.js +0 -1
  90. package/dist/clients/dispatch/runners/phpstan.js +0 -1
  91. package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  92. package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
  93. package/dist/clients/dispatch/runners/pyright.js +7 -4
  94. package/dist/clients/dispatch/runners/rubocop.js +0 -1
  95. package/dist/clients/dispatch/runners/ruff.js +0 -1
  96. package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
  97. package/dist/clients/dispatch/runners/shellcheck.js +0 -1
  98. package/dist/clients/dispatch/runners/shfmt.js +0 -1
  99. package/dist/clients/dispatch/runners/spellcheck.js +0 -1
  100. package/dist/clients/dispatch/runners/spotbugs.js +0 -1
  101. package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  102. package/dist/clients/dispatch/runners/stylelint.js +0 -1
  103. package/dist/clients/dispatch/runners/swiftlint.js +0 -1
  104. package/dist/clients/dispatch/runners/taplo.js +0 -1
  105. package/dist/clients/dispatch/runners/terragrunt.js +0 -1
  106. package/dist/clients/dispatch/runners/tflint.js +0 -1
  107. package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  108. package/dist/clients/dispatch/runners/trivy-config.js +0 -1
  109. package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  110. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  111. package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
  112. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  113. package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  114. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
  115. package/dist/clients/dispatch/runners/vale.js +0 -1
  116. package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  117. package/dist/clients/dispatch/runners/yamllint.js +0 -1
  118. package/dist/clients/dispatch/runners/zig-check.js +0 -1
  119. package/dist/clients/disposition-logger.js +3 -3
  120. package/dist/clients/effective-config.js +403 -0
  121. package/dist/clients/error-class.js +23 -0
  122. package/dist/clients/event-loop-hold.js +274 -0
  123. package/dist/clients/event-loop-monitor.js +66 -1
  124. package/dist/clients/extension-log.js +2 -2
  125. package/dist/clients/feature-hints.js +2 -1
  126. package/dist/clients/file-time.js +6 -1
  127. package/dist/clients/file-utils.js +266 -13
  128. package/dist/clients/finding-delivery-gate.js +55 -13
  129. package/dist/clients/format-service.js +6 -1
  130. package/dist/clients/formatters.js +303 -104
  131. package/dist/clients/freshness-cadence.js +17 -0
  132. package/dist/clients/generated-artifacts.js +114 -19
  133. package/dist/clients/generation-guard.js +4 -9
  134. package/dist/clients/git-guard.js +198 -48
  135. package/dist/clients/git-tracked-ignore.js +32 -1
  136. package/dist/clients/go-client.js +39 -0
  137. package/dist/clients/govulncheck-client.js +5 -1
  138. package/dist/clients/gradle-ktfmt-style.js +252 -0
  139. package/dist/clients/gzip-stage-write.js +7 -0
  140. package/dist/clients/hashline-anchor.js +424 -0
  141. package/dist/clients/hook-budgets.js +76 -0
  142. package/dist/clients/host-edit-normalize.js +5 -2
  143. package/dist/clients/host-ports.js +1 -1
  144. package/dist/clients/installer/index.js +200 -84
  145. package/dist/clients/installer/managed-tool-refresh.js +3 -1
  146. package/dist/clients/instance-reaper.js +108 -243
  147. package/dist/clients/instance-registry-lock.js +145 -0
  148. package/dist/clients/instance-registry.js +435 -122
  149. package/dist/clients/jscpd-client.js +6 -1
  150. package/dist/clients/language-profile.js +4 -2
  151. package/dist/clients/language-registry.js +595 -0
  152. package/dist/clients/latency-logger.js +179 -3
  153. package/dist/clients/ledger-bounds.js +13 -0
  154. package/dist/clients/lens-config.js +141 -30
  155. package/dist/clients/lens-engine.js +30 -37
  156. package/dist/clients/lens-flag-registry.js +67 -6
  157. package/dist/clients/lens-map.js +3 -1
  158. package/dist/clients/log-cleanup.js +2 -2
  159. package/dist/clients/lsp/aggregation.js +3 -1
  160. package/dist/clients/lsp/cascade-tier.js +4 -0
  161. package/dist/clients/lsp/client.js +672 -49
  162. package/dist/clients/lsp/config.js +504 -156
  163. package/dist/clients/lsp/diagnostic-binding.js +10 -5
  164. package/dist/clients/lsp/edits.js +15 -2
  165. package/dist/clients/lsp/index.js +1179 -247
  166. package/dist/clients/lsp/jvm-runtime.js +5 -0
  167. package/dist/clients/lsp/language.js +18 -169
  168. package/dist/clients/lsp/launch.js +38 -9
  169. package/dist/clients/lsp/pending-aux-coverage.js +113 -12
  170. package/dist/clients/lsp/server.js +426 -160
  171. package/dist/clients/lsp/session-roots.js +192 -0
  172. package/dist/clients/lsp/spawn-history.js +2 -7
  173. package/dist/clients/lsp/tsserver-sync.js +9 -2
  174. package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
  175. package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  176. package/dist/clients/lsp-mutation.js +179 -23
  177. package/dist/clients/map-with-concurrency.js +36 -0
  178. package/dist/clients/mcp/analyze.js +45 -10
  179. package/dist/clients/mcp/session.js +19 -17
  180. package/dist/clients/memory-sampler.js +28 -1
  181. package/dist/clients/message-end-attribution.js +30 -0
  182. package/dist/clients/metrics-history.js +21 -4
  183. package/dist/clients/middle-man-analysis.js +2 -4
  184. package/dist/clients/module-report.js +20 -45
  185. package/dist/clients/mutating-tool.js +651 -0
  186. package/dist/clients/mutation-attribution.js +368 -0
  187. package/dist/clients/mutation-bridge.js +240 -0
  188. package/dist/clients/ndjson-logger.js +247 -20
  189. package/dist/clients/observed-mutation-sources.js +101 -0
  190. package/dist/clients/observed-mutation.js +1215 -0
  191. package/dist/clients/opaque-mutation-scan.js +300 -40
  192. package/dist/clients/opengrep-config.js +4 -0
  193. package/dist/clients/package-manager.js +201 -44
  194. package/dist/clients/partial-edit-apply.js +359 -80
  195. package/dist/clients/path-attribution-telemetry.js +23 -7
  196. package/dist/clients/path-keyed-map.js +35 -3
  197. package/dist/clients/path-utils.js +460 -13
  198. package/dist/clients/persist-debounce.js +8 -1
  199. package/dist/clients/php-cs-fixer-config.js +114 -0
  200. package/dist/clients/pipeline.js +77 -24
  201. package/dist/clients/probe-home-state.js +228 -0
  202. package/dist/clients/process-bridge.js +66 -0
  203. package/dist/clients/process-singletons.js +210 -0
  204. package/dist/clients/process-snapshot.js +68 -0
  205. package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  206. package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  207. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  208. package/dist/clients/project-diagnostics/scanner.js +61 -24
  209. package/dist/clients/project-lens-config.js +393 -122
  210. package/dist/clients/project-snapshot.js +85 -35
  211. package/dist/clients/python-environment.js +217 -0
  212. package/dist/clients/python-provenance.js +639 -0
  213. package/dist/clients/quiet-window.js +6 -1
  214. package/dist/clients/read-bridge.js +13 -21
  215. package/dist/clients/read-expansion.js +9 -46
  216. package/dist/clients/read-guard-logger.js +39 -6
  217. package/dist/clients/read-guard-tool-lines.js +364 -212
  218. package/dist/clients/read-guard.js +95 -7
  219. package/dist/clients/recent-touches.js +17 -5
  220. package/dist/clients/resource-sampler.js +195 -96
  221. package/dist/clients/review-graph/builder.js +752 -132
  222. package/dist/clients/review-graph/service.js +55 -3
  223. package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  224. package/dist/clients/review-graph/tsconfig-paths.js +5 -1
  225. package/dist/clients/review-graph/workspace-modules.js +10 -74
  226. package/dist/clients/review-graph-logger.js +30 -4
  227. package/dist/clients/runtime-agent-end.js +18 -2
  228. package/dist/clients/runtime-context.js +49 -16
  229. package/dist/clients/runtime-coordinator.js +93 -6
  230. package/dist/clients/runtime-session.js +169 -34
  231. package/dist/clients/runtime-tool-call.js +183 -35
  232. package/dist/clients/runtime-tool-result.js +894 -241
  233. package/dist/clients/runtime-turn.js +1264 -74
  234. package/dist/clients/rust-client.js +31 -0
  235. package/dist/clients/safe-spawn.js +191 -28
  236. package/dist/clients/sanitize.js +7 -1
  237. package/dist/clients/scratch-tree-policy.js +1 -3
  238. package/dist/clients/security-scan-client.js +51 -17
  239. package/dist/clients/session-lifecycle.js +238 -42
  240. package/dist/clients/session-start-observability.js +79 -0
  241. package/dist/clients/sessionstart-logger.js +12 -3
  242. package/dist/clients/sg-runner.js +27 -15
  243. package/dist/clients/sgconfig.js +6 -1
  244. package/dist/clients/shared-checkout-guard.js +338 -0
  245. package/dist/clients/skills-resolver.js +105 -0
  246. package/dist/clients/smells-rollup.js +2 -2
  247. package/dist/clients/spawn-output-cap.js +37 -0
  248. package/dist/clients/spawn-timeout-cooldown.js +15 -1
  249. package/dist/clients/startup-scan.js +6 -7
  250. package/dist/clients/startup-timing.js +69 -4
  251. package/dist/clients/string-utils.js +33 -0
  252. package/dist/clients/subagent-mode.js +17 -4
  253. package/dist/clients/test-runner-client.js +586 -118
  254. package/dist/clients/test-runner-delivery.js +239 -0
  255. package/dist/clients/tool-definition.js +41 -1
  256. package/dist/clients/tool-policy.js +92 -22
  257. package/dist/clients/tree-sitter-cache.js +40 -40
  258. package/dist/clients/tree-sitter-client.js +97 -53
  259. package/dist/clients/tree-sitter-logger.js +19 -3
  260. package/dist/clients/tree-sitter-query-loader.js +81 -1
  261. package/dist/clients/tree-sitter-shared.js +51 -46
  262. package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  263. package/dist/clients/typos-config.js +5 -0
  264. package/dist/clients/user-notify.js +6 -2
  265. package/dist/clients/warm-attach.js +8 -10
  266. package/dist/clients/widget-state.js +191 -26
  267. package/dist/clients/word-index-logger.js +15 -3
  268. package/dist/clients/word-index-store.js +463 -0
  269. package/dist/clients/word-index.js +815 -125
  270. package/dist/clients/workspace-topology.js +25 -1
  271. package/dist/clients/zizmor-config.js +29 -6
  272. package/dist/index.js +68577 -53950
  273. package/dist/mcp/analyze-cli.js +3 -1
  274. package/dist/mcp/build-staleness.js +8 -7
  275. package/dist/mcp/cli.js +3 -1
  276. package/dist/mcp/server.js +133 -6
  277. package/dist/scripts/lib/process-scan.mjs +583 -0
  278. package/dist/scripts/lib/skills-predicate.mjs +129 -0
  279. package/dist/tools/effective-config.js +89 -0
  280. package/dist/tools/lens-diagnostics.js +99 -24
  281. package/dist/tools/lsp-diagnostics.js +81 -85
  282. package/dist/tools/lsp-navigation.js +40 -17
  283. package/dist/tools/shared.js +0 -1
  284. package/docs/agent-guide.md +68 -3
  285. package/docs/ast-grep_rules_catalog.md +41 -10
  286. package/docs/astplayground.md +26 -17
  287. package/docs/configuration.md +222 -0
  288. package/docs/dependencies.md +3 -3
  289. package/docs/features.md +44 -5
  290. package/docs/language-coverage.md +2 -2
  291. package/docs/pi-lens-fixer.md +25 -0
  292. package/docs/pi-lens-investigator.md +25 -0
  293. package/docs/pi-lens-reviewer.md +27 -0
  294. package/docs/pi-lens-subagent.md +38 -0
  295. package/docs/pi-lens-warden.md +55 -0
  296. package/docs/public-api-stability.md +359 -0
  297. package/docs/release-qa-baseline.md +182 -0
  298. package/docs/subagent-compat.md +110 -30
  299. package/docs/tree-sitter_rules_catalog.md +2 -2
  300. package/package.json +17 -21
  301. package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  302. package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  303. package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  304. package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  305. package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  306. package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  307. package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  308. package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  309. package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  310. package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  311. package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  312. package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  313. package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  314. package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  315. package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  316. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  317. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  318. package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  319. package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  320. package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  321. package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  322. package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  323. package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  324. package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  325. package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  326. package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  327. package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  328. package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  329. package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  330. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  331. package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  332. package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  333. package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  334. package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  335. package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  336. package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  337. package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  338. package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  339. package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  340. package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  341. package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  342. package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  343. package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  344. package/rules/ast-grep-rules/rules/no-important.yml +21 -0
  345. package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  346. package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  347. package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  348. package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  349. package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  350. package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  351. package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  352. package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  353. package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  354. package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  355. package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  356. package/scripts/analyze-pi-lens-logs.mjs +192 -1
  357. package/scripts/install-selftest.mjs +58 -2
  358. package/scripts/lib/skills-predicate.mjs +129 -0
  359. package/scripts/rpc-load-check.mjs +3 -1
@@ -0,0 +1,129 @@
1
+ // The ONE structural replica of pi's own skill-loading walk (#2626 review
2
+ // round 2, F1/F2).
3
+ //
4
+ // Two independent detectors used to answer "does this directory tree hold a
5
+ // skill pi would load" with two DIFFERENT, hand-rolled heuristics that both
6
+ // diverged from pi's real resolver:
7
+ // - `clients/skills-resolver.ts` (a `SKILL.md` one level down only, no
8
+ // recursion, no root `.md` acceptance, no symlink-follow, no dot/
9
+ // node_modules skip)
10
+ // - `scripts/install-selftest.mjs`'s `countSkillFiles` (recursed
11
+ // UNBOUNDED — would double-count a `SKILL.md` sitting inside another
12
+ // skill's own resource subdirectory — with no dot/node_modules skip, no
13
+ // root `.md` acceptance, no symlink handling)
14
+ // Neither matched `loadSkillsFromDirInternal` in
15
+ // `@earendil-works/pi-coding-agent` `dist/core/skills.js` (mirrored by
16
+ // `collectSkillEntries` in `dist/core/package-manager.js`, byte-identical
17
+ // walk, confirmed against the 0.85.1 tarball). This module is the fold: BOTH
18
+ // consumers now derive from `collectSkillEntryPaths` below.
19
+ //
20
+ // Discovery rules, replicated from `loadSkillsFromDirInternal` (verified
21
+ // against the extracted 0.85.1 `skills.js`):
22
+ // 1. If `dir` directly contains a (non-symlink-broken) `SKILL.md`, `dir` IS
23
+ // one skill root — return immediately, do NOT look at `dir`'s other
24
+ // entries or recurse into its subdirectories (a `SKILL.md` inside a
25
+ // skill's own reference/ subfolder is the skill's OWN resource, not a
26
+ // second skill).
27
+ // 2. Otherwise, skip dot-prefixed entries and `node_modules`; recurse into
28
+ // every remaining subdirectory (same two rules, one level down); at the
29
+ // OUTERMOST call only (`isRoot`), a loose `*.md` file directly in `dir`
30
+ // also counts (pi's `includeRootFiles`, true only for the top call).
31
+ // 3. A symlink is followed via `statSync` (broken symlink = skip) for BOTH
32
+ // the `SKILL.md` check and the directory/file classification in step 2.
33
+ //
34
+ // Deliberately NOT replicated (out of #2626's scope — packaging/path
35
+ // resolution, not skill-content validation):
36
+ // - `.gitignore`/`.ignore`/`.fdignore` matching inside the skills tree.
37
+ // - frontmatter parsing: pi additionally requires a non-empty `description`
38
+ // field before a discovered file becomes a loaded skill. A `SKILL.md`
39
+ // file that exists but fails THAT validation is a content-authoring
40
+ // defect a maintainer would see in normal use, not a silent zero.
41
+ // Both gaps mean this predicate can be a bit more OPTIMISTIC than pi's true
42
+ // count in a pathological case (an ignored or description-less file still
43
+ // counts as "found" here); it is never more PESSIMISTIC — every real skill
44
+ // pi loads is found by this walk too — which is the direction #2626 cares
45
+ // about (never claim "no skills" when pi would load one).
46
+ import * as fs from "node:fs";
47
+ import * as path from "node:path";
48
+ /**
49
+ * Symlink-aware entry classification, mirroring `loadSkillsFromDirInternal`'s
50
+ * own `entry.isSymbolicLink() ? statSync(fullPath) : entry`.
51
+ * @param {string} fullPath
52
+ * @param {import("node:fs").Dirent} entry
53
+ * @returns {"file" | "dir" | "other"}
54
+ */
55
+ export function resolvedEntryKind(fullPath, entry) {
56
+ if (entry.isSymbolicLink()) {
57
+ try {
58
+ const stats = fs.statSync(fullPath);
59
+ if (stats.isDirectory())
60
+ return "dir";
61
+ if (stats.isFile())
62
+ return "file";
63
+ return "other";
64
+ }
65
+ catch {
66
+ return "other"; // broken symlink
67
+ }
68
+ }
69
+ if (entry.isDirectory())
70
+ return "dir";
71
+ if (entry.isFile())
72
+ return "file";
73
+ return "other";
74
+ }
75
+ /**
76
+ * The structural walk over an ALREADY-READ directory listing — split out
77
+ * from `collectSkillEntryPaths` so a caller that already has `entries` (a
78
+ * top-level ENOENT/EACCES distinction, say) is not forced into a second
79
+ * `readdirSync`.
80
+ * @param {string} dir
81
+ * @param {import("node:fs").Dirent[]} entries
82
+ * @param {boolean} isRoot
83
+ * @returns {string[]} absolute paths of every `SKILL.md` (or, at the root,
84
+ * loose `.md`) file pi's own resolver would treat as a skill entry point.
85
+ */
86
+ export function scanEntriesForSkills(dir, entries, isRoot) {
87
+ for (const entry of entries) {
88
+ if (entry.name === "SKILL.md" &&
89
+ resolvedEntryKind(path.join(dir, entry.name), entry) === "file") {
90
+ return [path.join(dir, entry.name)];
91
+ }
92
+ }
93
+ const found = [];
94
+ for (const entry of entries) {
95
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
96
+ continue;
97
+ }
98
+ const fullPath = path.join(dir, entry.name);
99
+ const kind = resolvedEntryKind(fullPath, entry);
100
+ if (kind === "dir") {
101
+ found.push(...collectSkillEntryPaths(fullPath, false));
102
+ continue;
103
+ }
104
+ if (isRoot && kind === "file" && entry.name.endsWith(".md")) {
105
+ found.push(fullPath);
106
+ }
107
+ }
108
+ return found;
109
+ }
110
+ /**
111
+ * Every skill entry point under `dir`, recursively, per pi's discovery
112
+ * rules. Fail-closed: a `readdirSync` error at `dir` (absent, unreadable) or
113
+ * any recursed-into subdirectory returns `[]` for that branch rather than
114
+ * throwing — matching `loadSkillsFromDirInternal`'s own bare `try { … }
115
+ * catch {}` per directory.
116
+ * @param {string} dir
117
+ * @param {boolean} [isRoot]
118
+ * @returns {string[]}
119
+ */
120
+ export function collectSkillEntryPaths(dir, isRoot = true) {
121
+ let entries;
122
+ try {
123
+ entries = fs.readdirSync(dir, { withFileTypes: true });
124
+ }
125
+ catch {
126
+ return [];
127
+ }
128
+ return scanEntriesForSkills(dir, entries, isRoot);
129
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * effective_config pi tool (#2427) — the in-process half of
3
+ * `pilens_effective_config`.
4
+ *
5
+ * A thin wrapper over the `effectiveConfig()` engine seam
6
+ * (`clients/lens-engine.ts`), like `tools/symbol-search.ts` over
7
+ * `symbolSearch()`: the payload is byte-identical to the MCP mirror's because
8
+ * both render the SAME view object, and neither adapter shapes it. The whole
9
+ * reason the capability is an engine method rather than an MCP handler is that
10
+ * a second projection would be a second chance to disagree about which server
11
+ * is running.
12
+ */
13
+ import { Type } from "../clients/deps/typebox.js";
14
+ import { effectiveConfig, isEffectiveFileViewError, } from "../clients/lens-engine.js";
15
+ import { compactRenderResult } from "./render-compact.js";
16
+ /** One line per decision, for the compact renderer. */
17
+ function summarize(view) {
18
+ const documents = `${view.documents.length} config file(s)`;
19
+ if (!view.file)
20
+ return `effective_config — ${documents}`;
21
+ if (isEffectiveFileViewError(view.file)) {
22
+ return `effective_config — ${documents} · ${view.file.error}`;
23
+ }
24
+ const selected = view.file.servers.filter((entry) => entry.selected).length;
25
+ const denied = view.file.servers.filter((entry) => entry.reason === "disabled-by-config").length;
26
+ const tools = view.file.tools.filter((entry) => entry.selected).length;
27
+ // The denial clause is built first rather than nested inside the summary
28
+ // template: a conditional expression inside a template inside a template is
29
+ // the S3358/nested-template shape SonarCloud flags, and it hid the one
30
+ // number a reader is looking for (#2427 review round 2, F3).
31
+ const deniedClause = denied > 0 ? `, ${denied} denied by config` : "";
32
+ return (`effective_config ${view.file.path} — ${documents} · ` +
33
+ `${selected} server(s) selected${deniedClause} · ` +
34
+ `${tools} tool(s)`);
35
+ }
36
+ export function createEffectiveConfigTool(getProjectRoot) {
37
+ return {
38
+ name: "effective_config",
39
+ label: "Effective Config",
40
+ description: "The resolved pi-lens configuration with the provenance of every decision — which file and tier each setting came from, and the trust decision that applied. Pass `file` to also get, for that path, its language, every LSP server with the reason it was selected or denied (including which tier's config denied it, which a nearer file cannot lift), and the lint/format runners that would dispatch. Answers 'why is X running/selected' without reading logs. Redacted by construction: it reports sources, never values — no env values, no command arguments beyond the binary, and config paths are home-relative.",
41
+ promptSnippet: "Resolved config + provenance — why a server or tool is selected",
42
+ renderResult: compactRenderResult(({ details, isError }) => isError
43
+ ? "effective_config — failed"
44
+ : (details?.summary ?? "effective_config")),
45
+ parameters: Type.Object({
46
+ file: Type.Optional(Type.String({
47
+ description: "Path to explain: adds the resolved language plus the per-server and " +
48
+ "per-runner selection decisions for it. Must resolve inside `cwd` " +
49
+ "(a sibling package in the same monorepo does not count) — a file " +
50
+ "outside it is rejected with the `cwd` it was measured against; " +
51
+ "re-query with `cwd` set to that file's own workspace instead.",
52
+ })),
53
+ }),
54
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
55
+ const cwd = getProjectRoot() || ctx.cwd || ".";
56
+ const view = await effectiveConfig({
57
+ cwd,
58
+ ...(params.file === undefined ? {} : { file: params.file }),
59
+ redact: true,
60
+ });
61
+ const summary = summarize(view);
62
+ return {
63
+ content: [
64
+ {
65
+ type: "text",
66
+ text: `${summary}\n\n${JSON.stringify(view)}`,
67
+ },
68
+ ],
69
+ // Summary-shaped, like `symbol_search`'s: the full view is already
70
+ // in `content[0].text`, and `details` exists for the renderer.
71
+ // Duplicating a whole provenance dump into it would double the
72
+ // payload of every call for one line of output.
73
+ details: {
74
+ summary,
75
+ documents: view.documents.length,
76
+ provenance: view.provenance.length,
77
+ ...(view.file === undefined
78
+ ? {}
79
+ : isEffectiveFileViewError(view.file)
80
+ ? { fileError: view.file.error }
81
+ : {
82
+ file: view.file.path,
83
+ selectedServers: view.file.servers.filter((entry) => entry.selected).length,
84
+ }),
85
+ },
86
+ };
87
+ },
88
+ };
89
+ }
@@ -11,8 +11,10 @@
11
11
  import { promises as fs } from "node:fs";
12
12
  import * as fsSync from "node:fs";
13
13
  import * as path from "node:path";
14
+ import { BoundedLruCache } from "../clients/bounded-cache.js";
14
15
  import { Type } from "../clients/deps/typebox.js";
15
16
  import { anchorsForDiagnostic, applyDispositions, applyWeakDispositions, hasAnyDispositionMarks, hasStrictDispositionMarks, getDisposition, } from "../clients/diagnostic-dispositions.js";
17
+ import { DEPENDENCY_DRIFT_MAX_DELIVERIES } from "../clients/blocker-freshness.js";
16
18
  import { freshnessFromMtime } from "../clients/freshness.js";
17
19
  import { applyInlineSuppressions } from "../clients/dispatch/inline-suppressions.js";
18
20
  import { gateFindingsByPathFreshness } from "../clients/advisory-provenance.js";
@@ -34,7 +36,7 @@ import { ANALYZER_IDS, fetchFreshProjectDiagnostics, } from "../clients/project-
34
36
  import { loadBootstrapClients } from "../clients/bootstrap.js";
35
37
  import { generatedSkipNotice, scanTruncationNotice, } from "../clients/lens-engine.js";
36
38
  import { scanProjectDiagnostics } from "../clients/project-diagnostics/scanner.js";
37
- import { getFileDiagnosticSummaries, reconcileCorrelatedScanDiagnostics, reconcileScanDiagnostics, reconcileStaleWidgetDependencyBlockers, reconcileStaleWidgetFiles, widgetDiagnosticUri, } from "../clients/widget-state.js";
39
+ import { classifyDiagnosticTier, getFileDiagnosticSummaries, reconcileCorrelatedScanDiagnostics, reconcileScanDiagnostics, reconcileStaleWidgetDependencyBlockers, reconcileStaleWidgetFiles, widgetDiagnosticUri, } from "../clients/widget-state.js";
38
40
  import { logLatency } from "../clients/latency-logger.js";
39
41
  import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
40
42
  import { retagAuxiliaryDiagnostics } from "../clients/dispatch/auxiliary-lsp.js";
@@ -203,9 +205,15 @@ captureLspStatusRepaint, getRuntime) {
203
205
  const b = details?.totalBlocking ?? 0;
204
206
  const e = details?.totalErrors ?? 0;
205
207
  const w = details?.totalWarnings ?? 0;
208
+ // #2414: hint/info tier never contributes to `b + e + w` — a session
209
+ // with only style opinions is genuinely "clean" of code defects, but
210
+ // the one-line summary still names the advisory count so it isn't
211
+ // indistinguishable from a session with zero findings at all.
212
+ const adv = details?.totalAdvisories ?? 0;
206
213
  const files = details?.filesWithIssues ?? details?.filesChecked ?? 0;
207
214
  if (b + e + w === 0) {
208
- return `lens_diagnostics ${mode} — clean (${files} files)${coldSuffix}${failedSuffix}`;
215
+ const advisorySuffix = adv > 0 ? `, ${adv} hint${adv === 1 ? "" : "s"}` : "";
216
+ return `lens_diagnostics ${mode} — clean${advisorySuffix} (${files} files)${coldSuffix}${failedSuffix}`;
209
217
  }
210
218
  const parts = [`${b} blocking`];
211
219
  if (b === 0 && e > 0)
@@ -417,15 +425,12 @@ function appendProjectDiagnosticsDeltaLines(lines, cwd, report, severity, includ
417
425
  * TTL. Insertion-ordered LRU cap bounds resident source bytes; repeated
418
426
  * queries and multi-file reports that cite the same file reuse one read.
419
427
  */
420
- const CACHED_CONTENT_MEMO_CAP = 64;
421
- const cachedContentMemo = new Map();
428
+ export const CACHED_CONTENT_MEMO_CAP = 64;
429
+ const cachedContentMemo = new BoundedLruCache(CACHED_CONTENT_MEMO_CAP);
422
430
  function readContentForAnchors(filePath, stat) {
423
431
  const key = normalizeEphemeralMapKey(filePath);
424
432
  const hit = cachedContentMemo.get(key);
425
433
  if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) {
426
- // Refresh LRU position.
427
- cachedContentMemo.delete(key);
428
- cachedContentMemo.set(key, hit);
429
434
  return hit.content;
430
435
  }
431
436
  let content;
@@ -435,11 +440,6 @@ function readContentForAnchors(filePath, stat) {
435
440
  catch {
436
441
  return undefined;
437
442
  }
438
- if (cachedContentMemo.size >= CACHED_CONTENT_MEMO_CAP) {
439
- const oldest = cachedContentMemo.keys().next().value;
440
- if (oldest !== undefined)
441
- cachedContentMemo.delete(oldest);
442
- }
443
443
  cachedContentMemo.set(key, {
444
444
  mtimeMs: stat.mtimeMs,
445
445
  size: stat.size,
@@ -447,6 +447,11 @@ function readContentForAnchors(filePath, stat) {
447
447
  });
448
448
  return content;
449
449
  }
450
+ /** #2442 test-only: exercise the bounded content memo (LRU) directly,
451
+ * without a full lens_diagnostics dispatch. */
452
+ export function _readContentForAnchorsForTests(filePath, stat) {
453
+ return readContentForAnchors(filePath, stat);
454
+ }
450
455
  /**
451
456
  * ONE disposition seam for every cache-only lens_diagnostics surface —
452
457
  * mode=all summaries, mode=delta's actionable/quality warnings, the delta
@@ -548,7 +553,35 @@ function loadProjectRulePolicyMap(cwd) {
548
553
  * `clients/finding-delivery-gate.ts` surface `lens-diagnostics:mode-delta`.
549
554
  */
550
555
  function applyDeltaFreshnessGate(files, cwd, generatedAt) {
551
- if (!generatedAt)
556
+ // #2504 review round 4 (F1): an actionable-warnings report is no longer the
557
+ // product of exactly ONE pass. A deferred off-hook LSP pull upserts its
558
+ // per-file entries into whatever report is persisted when it lands, so one
559
+ // report can carry entries observed minutes apart. Judging the older half
560
+ // against the newer report-level stamp would pass an out-of-band edit made
561
+ // in between off as live -- exactly the leak this gate exists to stop. Each
562
+ // distinct stamp is gated on its own; the ordinary single-stamp report
563
+ // takes one pass, as before, and the original file order is preserved.
564
+ const stamps = new Set(files.map((file) => file.generatedAt ?? generatedAt ?? ""));
565
+ if (stamps.size > 1) {
566
+ const gatedByPath = new Map();
567
+ for (const stamp of stamps) {
568
+ const group = files.filter((file) => (file.generatedAt ?? generatedAt ?? "") === stamp);
569
+ for (const gated of applyDeltaFreshnessGate(group.map((file) => ({
570
+ filePath: file.filePath,
571
+ warnings: file.warnings,
572
+ })), cwd, stamp === "" ? undefined : stamp)) {
573
+ gatedByPath.set(gated.filePath, gated);
574
+ }
575
+ }
576
+ return files
577
+ .map((file) => gatedByPath.get(file.filePath))
578
+ .filter((entry) => entry !== undefined);
579
+ }
580
+ // Every entry shares one stamp here; prefer the per-entry one when it has
581
+ // it, so a merged report's single surviving half is still aged by its own.
582
+ const stampedAt = [...stamps][0];
583
+ const effectiveAt = stampedAt !== undefined && stampedAt !== "" ? stampedAt : generatedAt;
584
+ if (!effectiveAt)
552
585
  return files;
553
586
  const flat = [];
554
587
  for (const file of files) {
@@ -561,7 +594,7 @@ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
561
594
  store: "lens-diagnostics-delta",
562
595
  findings: flat,
563
596
  cwd,
564
- scannedAt: generatedAt,
597
+ scannedAt: effectiveAt,
565
598
  citedPath: (f) => f.filePath,
566
599
  onMissing: "drop",
567
600
  });
@@ -611,6 +644,9 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
611
644
  .filter((file) => includeFile(file.filePath))
612
645
  .map((file) => ({
613
646
  filePath: file.filePath,
647
+ // #2504 r4 (F1): carried through, so a merged report's older
648
+ // entries keep their own age all the way to the freshness gate.
649
+ generatedAt: file.generatedAt,
614
650
  warnings: applyRulePolicy(applyCachedDispositions(file.warnings ?? [], cwd, file.filePath), policyMap),
615
651
  }))
616
652
  .filter((file) => file.warnings.length > 0);
@@ -886,6 +922,7 @@ function summarizeDiagnostics(filePath, diagnostics, hasFinalSnapshot) {
886
922
  let blocking = 0;
887
923
  let errors = 0;
888
924
  let warnings = 0;
925
+ let advisories = 0;
889
926
  for (const diagnostic of diagnostics) {
890
927
  // #1641: a demoted past-EOF entry keeps its severity for display but
891
928
  // drops out of the tallies — same reasoning as widget-state's `isBlocking`.
@@ -893,24 +930,28 @@ function summarizeDiagnostics(filePath, diagnostics, hasFinalSnapshot) {
893
930
  continue;
894
931
  if (diagnostic.semantic === "blocking")
895
932
  blocking++;
896
- if (diagnostic.severity === "error")
933
+ // #2414: severity → tier classification is shared with
934
+ // `countDiagnostics`/`countAdvisories` in `clients/widget-state.ts` via
935
+ // `classifyDiagnosticTier` — mode=all reads that store's tally and
936
+ // mode=full reads this one, so the two must agree. `hint`/`info` no
937
+ // longer inflate `warnings` (they used to, matching a pre-#2414
938
+ // `countDiagnostics`, so a hint-only file scored 0/0/0 there too and the
939
+ // `withIssues` filter below dropped it — that gap is now closed via
940
+ // `advisories`, not by re-folding hints into `warnings`).
941
+ const tier = classifyDiagnosticTier(diagnostic);
942
+ if (tier === "error")
897
943
  errors++;
898
- // #1777: `hint` and `info` tally alongside `warning`, matching
899
- // `countDiagnostics` in `clients/widget-state.ts` — mode=all reads that
900
- // tally and mode=full reads this one, so the two must agree. An exact
901
- // `=== "warning"` test scored a hint-only file 0/0/0, the `withIssues`
902
- // filter below then dropped it, and mode=full rendered "No issues" for a
903
- // file whose own `details` still carried the diagnostic.
904
- else if (diagnostic.severity === "warning" ||
905
- diagnostic.severity === "hint" ||
906
- diagnostic.severity === "info")
944
+ else if (tier === "warning")
907
945
  warnings++;
946
+ else if (tier === "advisory")
947
+ advisories++;
908
948
  }
909
949
  return {
910
950
  filePath,
911
951
  blocking,
912
952
  errors,
913
953
  warnings,
954
+ advisories,
914
955
  hasFinalSnapshot,
915
956
  diagnostics,
916
957
  };
@@ -929,6 +970,9 @@ const UNCONFIRMED_REASON_SENTENCE = {
929
970
  // completely different failure from a timeout. Must never collapse into
930
971
  // "within budget", the exact string this PR exists to stop misrendering.
931
972
  binding_mismatch: "the file changed on disk since this result was computed",
973
+ // #2052: no server was asked. Say WHY and say it is permanent for this
974
+ // path, so the reader does not read an empty result as "clean" or retry it.
975
+ outside_project_root: "the file is outside every initialized session project root, so no language server was asked",
932
976
  };
933
977
  // Short per-reason label used only when MORE than one reason is present, so
934
978
  // the note can still say "N timed out, M errored" instead of collapsing to
@@ -940,6 +984,7 @@ const UNCONFIRMED_REASON_COUNT_LABEL = {
940
984
  service_destroyed: "service reset mid-sweep",
941
985
  error: "errored",
942
986
  binding_mismatch: "stale binding",
987
+ outside_project_root: "outside project root",
943
988
  };
944
989
  /**
945
990
  * #1618: a result predating the `unconfirmedReason` field (a legacy/test
@@ -1835,6 +1880,13 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
1835
1880
  }
1836
1881
  // @delivery-surface: lens-diagnostics:mode-all
1837
1882
  function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(), detailOverrides = { mode: "all" }, staleDropped = 0, pathsScope, dependencyDemoted = 0) {
1883
+ // #2275 review F2: the widget footer stops DRAWING a dependency-drift
1884
+ // demotion once it hits `DEPENDENCY_DRIFT_MAX_DELIVERIES` unconfirmed
1885
+ // deliveries, but the record stays here (dropping it would make an
1886
+ // unconfirmed LSP error read as clean). Say so, through the same note
1887
+ // channel as the demotion itself, so the agent knows why the footer went
1888
+ // quiet and that a re-run can still confirm the finding.
1889
+ const footerCapped = summaries.reduce((n, s) => n + (s.diagnostics ?? []).filter((d) => d.footerRetired === true).length, 0);
1838
1890
  // Files changed/deleted since their diagnostics were recorded have already
1839
1891
  // been dropped by reconcileStaleWidgetFiles; note them so the agent knows
1840
1892
  // those aren't "clean", just un-rescanned (use mode=full to refresh).
@@ -1843,6 +1895,9 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1843
1895
  : "") +
1844
1896
  (dependencyDemoted > 0
1845
1897
  ? ` (${dependencyDemoted} blocking finding${dependencyDemoted === 1 ? "" : "s"} demoted to [stale] — an imported file changed since; re-run to confirm)`
1898
+ : "") +
1899
+ (footerCapped > 0
1900
+ ? ` (${footerCapped} demoted finding${footerCapped === 1 ? "" : "s"} capped after ${DEPENDENCY_DRIFT_MAX_DELIVERIES} unconfirmed deliveries — no longer shown in the pi-lens footer, still listed here; re-run to confirm)`
1846
1901
  : "");
1847
1902
  // mode=full already actively scanned exactly the requested paths, so a zero
1848
1903
  // result there IS a legitimate clean read — the cache-only note only applies
@@ -1897,11 +1952,18 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1897
1952
  const withIssues = visibleSummaries.filter((s) => {
1898
1953
  if (severity === "error")
1899
1954
  return s.blocking > 0 || s.errors > 0;
1955
+ // #2414: a "warning" filter must not admit hint/info — that is exactly
1956
+ // the "present hints as warnings" defect this issue exists to close.
1900
1957
  if (severity === "warning")
1901
1958
  return s.warnings > 0;
1959
+ // severity: "all" — a hint/info-only file (`advisories > 0`,
1960
+ // warnings === 0) must still surface here, or the #2414 fix that stops
1961
+ // hints inflating `warnings` would silently drop that file from the
1962
+ // detailed listing instead of just from the compact warning tally.
1902
1963
  return (s.blocking > 0 ||
1903
1964
  s.errors > 0 ||
1904
1965
  s.warnings > 0 ||
1966
+ s.advisories > 0 ||
1905
1967
  (s.diagnostics ?? []).some((d) => d.stale));
1906
1968
  });
1907
1969
  const pathsNote = isFullMode ? "" : pathsScopeCacheOnlyNote(pathsScope);
@@ -1926,6 +1988,7 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1926
1988
  let totalBlocking = 0;
1927
1989
  let totalErrors = 0;
1928
1990
  let totalWarnings = 0;
1991
+ let totalAdvisories = 0;
1929
1992
  for (const s of sorted) {
1930
1993
  const rel = path.relative(cwd, s.filePath);
1931
1994
  const parts = [];
@@ -1935,6 +1998,11 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1935
1998
  parts.push(`${s.errors}E`);
1936
1999
  if (s.warnings > 0)
1937
2000
  parts.push(`${s.warnings}W`);
2001
+ // #2414: hint/info never inflate `warnings`, but a hint-only row still
2002
+ // needs a reason to be listed — otherwise it shows a bare filename with
2003
+ // no count at all.
2004
+ if (s.advisories > 0)
2005
+ parts.push(`${s.advisories} hint${s.advisories === 1 ? "" : "s"}`);
1938
2006
  if (!s.hasFinalSnapshot)
1939
2007
  parts.push(`(pending)`);
1940
2008
  const staleCount = (s.diagnostics ?? []).filter((d) => d.stale).length;
@@ -1980,6 +2048,7 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
1980
2048
  totalBlocking += s.blocking;
1981
2049
  totalErrors += s.errors;
1982
2050
  totalWarnings += s.warnings;
2051
+ totalAdvisories += s.advisories;
1983
2052
  }
1984
2053
  // #1799: `totalBlocking` and `totalErrors` count the same findings UNLESS a
1985
2054
  // dependency-drift demotion (#1631) has revoked blocking authority from an
@@ -2000,6 +2069,11 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
2000
2069
  totalWarnings > 0
2001
2070
  ? ` ${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`
2002
2071
  : null,
2072
+ // #2414: reported separately from `warnings` — hint/info are style
2073
+ // opinions, not defects, and must not read as unresolved code issues.
2074
+ totalAdvisories > 0
2075
+ ? ` ${totalAdvisories} hint/info (style — not counted as warnings)`
2076
+ : null,
2003
2077
  ]
2004
2078
  .filter(Boolean)
2005
2079
  .join("\n");
@@ -2013,6 +2087,7 @@ function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(),
2013
2087
  totalBlocking,
2014
2088
  totalErrors,
2015
2089
  totalWarnings,
2090
+ totalAdvisories,
2016
2091
  staleDropped,
2017
2092
  },
2018
2093
  };