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,11 +1,25 @@
1
1
  /** Bounded, process-local telemetry for behavior degraded during one session. */
2
2
  import { logExtension } from "./extension-log.js";
3
- import { LEDGER_FIELD_MAX, truncateForLedger } from "./ledger-bounds.js";
3
+ import { DEGRADATION_ENTRIES_PER_KIND, LEDGER_FIELD_MAX, truncateForLedger, } from "./ledger-bounds.js";
4
4
  import { logLatency } from "./latency-logger.js";
5
- import { getSinkWriteFailures, resetSinkWriteFailures, } from "./ndjson-logger.js";
5
+ import { getSinkRotations, getSinkWriteFailures, resetSinkRotations, resetSinkWriteFailures, } from "./ndjson-logger.js";
6
+ // #2146: pulled at READ time, never pushed. `process-singletons.ts` is a
7
+ // dependency leaf on purpose — it cannot import this module without closing a
8
+ // no-client-cycles cycle through instance-registry/instance-reaper — so the
9
+ // ledger reaches IN for its reset log, the same inversion `getSinkWriteFailures`
10
+ // above uses.
11
+ import { getProcessSingletonResets, PROCESS_SINGLETON_RESET_KIND, } from "./process-singletons.js";
12
+ // #2506: same inversion, same reason — the log-dir resolver cannot import this
13
+ // module (directly OR dynamically). THIS module is on a no-client-cycles cycle
14
+ // via extension-log.ts; the resolver's whole correctness argument is that it
15
+ // sits off every cycle, so an edge in that direction would put it back on one.
16
+ // `getGlobalPiLensLogDir()`'s probe-home-redirect event is therefore written
17
+ // into `probe-home-state.ts` and read back here instead. See that module's doc
18
+ // comment for the full account.
19
+ import { getProbeHomeRedirectEvent } from "./probe-home-state.js";
6
20
  // Re-exported so existing importers keep one name for the ledger's bound.
7
21
  export { LEDGER_FIELD_MAX, truncateForLedger };
8
- const ENTRIES_PER_KIND = 20;
22
+ const ENTRIES_PER_KIND = DEGRADATION_ENTRIES_PER_KIND;
9
23
  const MAX_DISTINCT_KINDS = 32;
10
24
  const OVERFLOW_KIND = "other";
11
25
  const groups = new Map();
@@ -59,7 +73,7 @@ export function recordDegradationOnce(record) {
59
73
  return;
60
74
  onceKeys.add(key);
61
75
  if (recordDegradation({ kind, subject, reason: record.reason })) {
62
- logDurableDegradation(kind, subject, 1);
76
+ logDurableDegradation(kind, subject, 1, record.metadata, record.code);
63
77
  }
64
78
  }
65
79
  catch (error) {
@@ -108,7 +122,7 @@ export function incrementDegradationCount(record) {
108
122
  // Durable rows use the summary's admission and emit the first event and
109
123
  // power-of-two milestones only, keeping the sink bounded.
110
124
  if (admitted && isPowerOfTwo(count)) {
111
- logDurableDegradation(kind, subject, count);
125
+ logDurableDegradation(kind, subject, count, record.metadata, record.code);
112
126
  }
113
127
  return count === 1;
114
128
  }
@@ -124,15 +138,37 @@ export function incrementDegradationCount(record) {
124
138
  * redaction, and write queue. The subject and kind were already bounded by the
125
139
  * ledger before reaching this seam.
126
140
  */
127
- function logDurableDegradation(kind, subject, count) {
141
+ function logDurableDegradation(kind, subject, count, metadata, code) {
142
+ const boundedMetadata = boundLedgerMetadata(metadata);
128
143
  logLatency({
129
144
  type: "phase",
130
145
  phase: "degradation_ledger",
131
146
  filePath: subject,
132
147
  durationMs: 0,
133
- metadata: { kind, subject, count, ledgerGeneration },
148
+ metadata: {
149
+ ...boundedMetadata,
150
+ kind,
151
+ subject,
152
+ count,
153
+ ledgerGeneration,
154
+ // Reserved row field, written AFTER the bounded caller metadata so the
155
+ // `MAX_METADATA_KEYS` cap cannot evict the stable code (#2418).
156
+ ...(code ? { code } : {}),
157
+ },
134
158
  });
135
159
  }
160
+ const MAX_METADATA_KEYS = 8;
161
+ function boundLedgerMetadata(metadata) {
162
+ if (!metadata)
163
+ return {};
164
+ const entries = Object.entries(metadata);
165
+ const kept = entries.slice(0, MAX_METADATA_KEYS);
166
+ const bounded = Object.fromEntries(kept.map(([key, value]) => [key, truncateForLedger(value)]));
167
+ const dropped = entries.length - kept.length;
168
+ if (dropped > 0)
169
+ bounded.metadataDropped = dropped;
170
+ return bounded;
171
+ }
136
172
  function isPowerOfTwo(value) {
137
173
  return value > 0 && (value & (value - 1)) === 0;
138
174
  }
@@ -165,6 +201,70 @@ export function getDegradationSummary() {
165
201
  })),
166
202
  });
167
203
  }
204
+ // #2505, same read-time fold: a mid-session rotation is visible without
205
+ // this module writing about it through the very sink family it is
206
+ // reporting on — see the `log-sink-rotated` doc comment on
207
+ // `DegradationKind`.
208
+ const sinkRotations = getSinkRotations();
209
+ const rotated = sinkRotations.filter((sink) => sink.rotationCount > 0);
210
+ if (rotated.length > 0) {
211
+ summary.push({
212
+ kind: "log-sink-rotated",
213
+ count: rotated.reduce((total, sink) => total + sink.rotationCount, 0),
214
+ droppedCount: 0,
215
+ latestReasons: rotated.map((sink) => ({
216
+ subject: truncateForLedger(sink.file),
217
+ reason: truncateForLedger(`${sink.rotationCount} rotation(s) at the configured byte bound`),
218
+ })),
219
+ });
220
+ }
221
+ // A rotation the writer ATTEMPTED and could not complete is a different
222
+ // fact from a rotation that happened, and the only signal that a sink is
223
+ // growing past its bound right now (#2505 review F2).
224
+ const rotateFailed = sinkRotations.filter((sink) => sink.failureCount > 0);
225
+ if (rotateFailed.length > 0) {
226
+ summary.push({
227
+ kind: "log-sink-rotate-failed",
228
+ count: rotateFailed.reduce((total, sink) => total + sink.failureCount, 0),
229
+ droppedCount: 0,
230
+ latestReasons: rotateFailed.map((sink) => ({
231
+ subject: truncateForLedger(sink.file),
232
+ reason: truncateForLedger(`${sink.failureCount} failed rotation attempt(s); this sink is growing past its byte bound`),
233
+ })),
234
+ });
235
+ }
236
+ // #2146, same read-time fold: process-singleton resets live in the leaf
237
+ // module's own bounded log. One entry per family, so this group's count is
238
+ // the number of families this build could not adopt, never an event tally.
239
+ const singletonResets = getProcessSingletonResets();
240
+ if (singletonResets.length > 0) {
241
+ summary.push({
242
+ kind: PROCESS_SINGLETON_RESET_KIND,
243
+ count: singletonResets.length,
244
+ droppedCount: 0,
245
+ latestReasons: singletonResets.map((reset) => ({
246
+ subject: truncateForLedger(reset.family),
247
+ reason: truncateForLedger(reset.reason),
248
+ })),
249
+ });
250
+ }
251
+ // #2506, same read-time fold: `getGlobalPiLensLogDir()`'s probe-home redirect
252
+ // fires at most once per process (see `probe-home-state.ts`), so this is a
253
+ // presence check, not a tally.
254
+ const probeHomeRedirect = getProbeHomeRedirectEvent();
255
+ if (probeHomeRedirect) {
256
+ summary.push({
257
+ kind: "global-dir-probe-redirect",
258
+ count: 1,
259
+ droppedCount: 0,
260
+ latestReasons: [
261
+ {
262
+ subject: truncateForLedger(probeHomeRedirect.probeHome),
263
+ reason: truncateForLedger(`PI_LENS_HOME unset with cwd in an agent worktree/tmp probe context (${probeHomeRedirect.cwd}), or PILENS_PROBE=1 forced it; LOGS redirected away from the real home directory (tools, bin and instances.json are unaffected)`),
264
+ },
265
+ ],
266
+ });
267
+ }
168
268
  return summary;
169
269
  }
170
270
  function isRenderableSummary(value) {
@@ -183,6 +283,34 @@ function isRenderableSummary(value) {
183
283
  typeof entry.reason === "string"));
184
284
  });
185
285
  }
286
+ /**
287
+ * Kinds that record something the system did ON PURPOSE, correctly, and
288
+ * that a reader only needs a tally of — never a call to action (#2505
289
+ * review). A routine log rotation at the configured bound is the writer
290
+ * working as designed; giving it the same warning marker a real
291
+ * degradation gets trains the reader to ignore the marker. The FAILED
292
+ * rotation is the line that has to stand out, so it is deliberately NOT
293
+ * in this set.
294
+ */
295
+ const INFORMATIONAL_DEGRADATION_KINDS = new Set([
296
+ "log-sink-rotated",
297
+ // #2504 review round 8 (S1): both fire on the ordinary re-edit cadence
298
+ // (a carry-forward window closing, or a session boundary) and are
299
+ // self-healing -- the next turn's analysis or deferral re-observes the
300
+ // file -- so a `⚠` overstates them the same way a routine log rotation
301
+ // would.
302
+ "actionable-warnings-inband-superseded",
303
+ "actionable-warnings-deferred-superseded",
304
+ // #2523: a hook await abandoned because the session is tearing down. The
305
+ // teardown signal firing is the design working, so it is a tally; the
306
+ // BUDGET blow-out (`hook-await-exceeded`) is the line that has to stand
307
+ // out, and a caller's own Escape records nothing at all.
308
+ "hook-await-abandoned",
309
+ // #2524: can fire on a query that ultimately settles `ok` (see the kind's
310
+ // doc comment above) and is frequent/self-healing by design — a `⚠` would
311
+ // cry wolf on the sampler's ordinary best-effort data loss.
312
+ "resource-sampler-scanner-escalated",
313
+ ]);
186
314
  export function renderDegradationLines(summary = getDegradationSummary()) {
187
315
  if (!isRenderableSummary(summary))
188
316
  return [];
@@ -191,6 +319,9 @@ export function renderDegradationLines(summary = getDegradationSummary()) {
191
319
  return [
192
320
  "Degradations:",
193
321
  ...summary.map((group) => {
322
+ if (INFORMATIONAL_DEGRADATION_KINDS.has(group.kind)) {
323
+ return ` ${group.kind}: ${group.count}`;
324
+ }
194
325
  const latest = group.latestReasons.at(-1);
195
326
  return ` ⚠ ${group.kind}: ${group.count}${latest ? ` — ${latest.subject}: ${latest.reason}` : ""}`;
196
327
  }),
@@ -218,6 +349,33 @@ export function resetDegradationLedger() {
218
349
  // process-lifetime latch too — it re-arms alongside the rest of the
219
350
  // ledger rather than surviving past the session that observed it.
220
351
  resetSinkWriteFailures();
352
+ // #2505, same catalog shape 17 re-arm: a rotation tally recurs (new writes
353
+ // keep crossing the bound), so clearing it costs nothing and a later
354
+ // session re-observes the fact fresh.
355
+ resetSinkRotations();
356
+ // #2146 review F3: the OTHER pulled source, `getProcessSingletonResets()`,
357
+ // deliberately does NOT re-arm here, and the difference from its neighbour
358
+ // above is the point. A sink write failure recurs — new writes fail, so
359
+ // clearing the tally costs nothing and a later session re-observes the
360
+ // problem. A process-singleton reset happens once, at module-evaluation
361
+ // time, and cannot recur: after it, the container holds only compatible
362
+ // cells. Clearing it would show the fact in the first session's
363
+ // `pilens_health` and hide it from every session after, which is exactly
364
+ // when someone reads that line. The row is bounded independently of the
365
+ // session (one entry per family, capped at 16), so leaving it costs a fixed
366
+ // handful of lines and keeps a process-scope fact visible for the process's
367
+ // life. Deliberate exception to catalog shape 17, not an oversight.
368
+ //
369
+ // `getProbeHomeRedirectEvent()` (#2506) is the same shape as the
370
+ // process-singleton case above and for the same reason: the redirect is
371
+ // resolved at most once per PROCESS (memoized in the `globalThis` slot
372
+ // `probe-home-state.ts` owns, not a session-scoped one), so it cannot
373
+ // recur within this process's life either — clearing it here would hide
374
+ // the fact from every session after the first in the same probe process.
221
375
  }
222
- export const DEGRADATION_ENTRIES_PER_KIND = ENTRIES_PER_KIND;
376
+ // Re-exported so every existing importer keeps its specifier. The value now
377
+ // lives in the `ledger-bounds.js` leaf (#2426), so a producer that only needs
378
+ // the BOUND does not have to import the ledger — and cannot be broken by a test
379
+ // that mocks it wholesale.
380
+ export { DEGRADATION_ENTRIES_PER_KIND };
223
381
  export const DEGRADATION_MAX_DISTINCT_KINDS = MAX_DISTINCT_KINDS;
@@ -153,3 +153,17 @@ export function formatRetirementNote(deadLines) {
153
153
  return (`Retired after this delivery${cited}: the file no longer has these lines, ` +
154
154
  "so this finding cannot be re-confirmed and will not be served again.");
155
155
  }
156
+ /**
157
+ * The one-line note a surface appends when it RETIRES a dependency-drift
158
+ * demotion at its delivery cap (#1950). Deliberately worded differently from
159
+ * {@link formatRetirementNote}: that one means the finding is provably
160
+ * unrecoverable (the cited lines are gone), while THIS retirement is a
161
+ * bounded-noise cap on a record that is still recoverable — a fresh dispatch
162
+ * of the file could still confirm or clear it. Saying "cannot be re-confirmed"
163
+ * here would be false.
164
+ */
165
+ export function formatDeliveryCapNote(deliveryCount) {
166
+ return (`Not shown again after ${deliveryCount} deliveries with no re-run: ` +
167
+ "this finding may still be accurate, but re-serving it further adds no " +
168
+ "new information. A fresh dispatch of this file will re-confirm or clear it.");
169
+ }
@@ -11,9 +11,11 @@
11
11
  import { createSubsystemLogger } from "./extension-log.js";
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
+ import { mapWithConcurrency } from "./map-with-concurrency.js";
14
15
  import { findNodeToolBinary } from "./package-manager.js";
15
16
  import { isFullyQualified } from "./path-utils.js";
16
17
  import { safeSpawnAsync } from "./safe-spawn.js";
18
+ import { compareOrdinal } from "./string-utils.js";
17
19
  import { createAvailabilityChecker, discoverManagedTool, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
18
20
  import { createAvailabilityLatch, } from "./dispatch/runners/utils/availability-policy.js";
19
21
  /**
@@ -137,28 +139,16 @@ async function resolvedCommandIsStale(resolved, spawnableCache) {
137
139
  }
138
140
  /**
139
141
  * Run `mapper` over `items` with at most `concurrency` in flight at once.
142
+ *
140
143
  * Exported (#1810 review F6) so `clients/dispatch/runners/biome-check.ts`'s
141
144
  * `resolveBiomeFixKinds` can reuse this exact worker-pool shape for its
142
- * `biome explain` fan-out instead of carrying a second copy — this file
143
- * predates that need but is otherwise unrelated to it; a shared home for
144
- * this one helper wasn't worth a whole new module for two call sites.
145
+ * `biome explain` fan-out instead of carrying a second copy. #2504 moved the
146
+ * VALUE to the zero-dependency `map-with-concurrency.js` leaf — a third caller
147
+ * (`runtime-turn.ts`) needs the pool without pulling this analyzer client into
148
+ * the eager startup graph — and re-exports it here so every existing importer
149
+ * keeps its specifier. This stays the helper's documented home.
145
150
  */
146
- export async function mapWithConcurrency(items, concurrency, mapper) {
147
- if (items.length === 0)
148
- return;
149
- let nextIndex = 0;
150
- const workerCount = Math.max(1, Math.min(concurrency, items.length));
151
- const worker = async () => {
152
- while (true) {
153
- const index = nextIndex++;
154
- if (index >= items.length)
155
- return;
156
- await mapper(items[index]);
157
- }
158
- };
159
- const workers = Array.from({ length: workerCount }, () => worker());
160
- await Promise.all(workers);
161
- }
151
+ export { mapWithConcurrency };
162
152
  // --- Client ---
163
153
  export class DependencyChecker {
164
154
  madgeAvailability = createAvailabilityChecker("madge", ".cmd", ["--version"], {
@@ -501,8 +491,14 @@ export class DependencyChecker {
501
491
  const existing = this.checkInFlight.get(key);
502
492
  if (existing)
503
493
  return existing;
494
+ // Identity-guarded release (#1968's pattern): delete only if THIS run is
495
+ // still the registered one. A bare delete-by-key lets a late-settling run
496
+ // evict a live successor a second writer registered under the same key
497
+ // mid-flight, after which the next caller starts a duplicate check.
504
498
  const promise = this.runCheckFile(normalized, projectRoot, gen).finally(() => {
505
- this.checkInFlight.delete(key);
499
+ if (this.checkInFlight.get(key) === promise) {
500
+ this.checkInFlight.delete(key);
501
+ }
506
502
  });
507
503
  this.checkInFlight.set(key, promise);
508
504
  return promise;
@@ -781,8 +777,14 @@ export class DependencyChecker {
781
777
  // does, and the two overlap (runtime-session, fresh-fetch), so it takes a
782
778
  // generation too — claimed before the spawn, as late as a scan can.
783
779
  const gen = ++this.opGeneration;
780
+ // Identity-guarded release (#1968's pattern): delete only if THIS run is
781
+ // still the registered one. A bare delete-by-key lets a late-settling run
782
+ // evict a live successor a second writer registered under the same key
783
+ // mid-flight, after which the next caller starts a duplicate scan.
784
784
  const promise = this.runScanProject(projectRoot, gen).finally(() => {
785
- this.scanInFlight.delete(projectRoot);
785
+ if (this.scanInFlight.get(projectRoot) === promise) {
786
+ this.scanInFlight.delete(projectRoot);
787
+ }
786
788
  });
787
789
  this.scanInFlight.set(projectRoot, promise);
788
790
  return promise;
@@ -830,7 +832,11 @@ export class DependencyChecker {
830
832
  const seen = new Set();
831
833
  let output = `[Circular Deps] ${circular.length} cycle(s) found:\n`;
832
834
  for (const dep of circular) {
833
- const cycleKey = dep.path.sort((a, b) => a.localeCompare(b)).join("→");
835
+ // Copy before sorting: `dep.path` is rendered verbatim below (and by
836
+ // other CircularDep consumers, e.g. madge.ts's diagnostic renderer),
837
+ // so the dedupe key must not reorder — let alone mutate in place —
838
+ // the path a user reads.
839
+ const cycleKey = [...dep.path].sort(compareOrdinal).join("→");
834
840
  if (seen.has(cycleKey))
835
841
  continue;
836
842
  seen.add(cycleKey);
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Centralized accessor for `minimatch`. See ./typescript.ts for the rationale.
3
3
  */
4
- export { minimatch } from "minimatch";
4
+ export { minimatch, Minimatch } from "minimatch";
@@ -62,6 +62,7 @@
62
62
  * healing direction never re-trigger a resync — only the rising edge does.
63
63
  */
64
64
  import * as fs from "node:fs";
65
+ import { BoundedFifoMap } from "./bounded-cache.js";
65
66
  import { logLatency } from "./latency-logger.js";
66
67
  import { toProjectRelativePath } from "./path-utils.js";
67
68
  import { STALE_LINE_MARKER } from "./stale-marker.js";
@@ -90,16 +91,12 @@ export function createLineCountCache() {
90
91
  // mismatched mtime always recomputes (see the module doc comment). Bounded
91
92
  // via FIFO eviction (mirrors `READ_GUARD_MAX_FILES`'s precedent) so a long
92
93
  // session's file count cannot grow this without limit.
93
- const MAX_SHARED_CACHE_ENTRIES = 512;
94
- const sharedLineCountCache = new Map();
94
+ export const MAX_SHARED_CACHE_ENTRIES = 512;
95
+ const sharedLineCountCache = new BoundedFifoMap(MAX_SHARED_CACHE_ENTRIES);
95
96
  function rememberInSharedCache(cache, filePath, entry) {
96
- if (cache === sharedLineCountCache &&
97
- cache.size >= MAX_SHARED_CACHE_ENTRIES &&
98
- !cache.has(filePath)) {
99
- const oldest = cache.keys().next().value;
100
- if (oldest !== undefined)
101
- cache.delete(oldest);
102
- }
97
+ // `cache` is either the shared BoundedFifoMap (bounds itself on `set`) or a
98
+ // test-isolated plain `Map` (unbounded by design) — either way, this is
99
+ // just a write.
103
100
  cache.set(filePath, entry);
104
101
  }
105
102
  /** Test-only: drop the shared memo between test files/cases. */
@@ -117,6 +114,12 @@ export function _resetSharedLineCountCacheForTests() {
117
114
  export function _seedSharedLineCountCacheForTests(filePath, entry) {
118
115
  sharedLineCountCache.set(filePath, entry);
119
116
  }
117
+ /** #2442 test-only: membership check that bypasses the real-stat requirement
118
+ * `getCachedLineCount` imposes on every call — proves capacity eviction
119
+ * directly, without needing 513 real files on disk. */
120
+ export function _sharedLineCountCacheHasForTests(filePath) {
121
+ return sharedLineCountCache.has(filePath);
122
+ }
120
123
  /**
121
124
  * Count newline bytes in `filePath` via bounded raw reads — never a full
122
125
  * `readFileSync`/UTF-8 decode of the whole file. `0x0A` (LF) never appears as
@@ -5,10 +5,11 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { isTestMode } from "./env-utils.js";
8
- import { getGlobalPiLensDir } from "./file-utils.js";
8
+ import { getGlobalPiLensLogDir } from "./probe-home-state.js";
9
+ import { getMaxLogSizeMB } from "./log-cleanup.js";
9
10
  import { createNdjsonLogger } from "./ndjson-logger.js";
10
11
  function getLogDir() {
11
- return path.join(getGlobalPiLensDir(), "logs");
12
+ return path.join(getGlobalPiLensLogDir(), "logs");
12
13
  }
13
14
  function getLogFile() {
14
15
  const date = new Date().toISOString().split("T")[0];
@@ -24,8 +25,17 @@ export function getDiagnosticLogger() {
24
25
  }
25
26
  export function createDiagnosticLogger() {
26
27
  // Lazy filePath: the log file is keyed on the current date, resolved per
27
- // drain so a long-lived logger rolls over at midnight.
28
- const writer = createNdjsonLogger({ filePath: () => getLogFile() });
28
+ // drain so a long-lived logger rolls over at midnight. A daily rollover
29
+ // is NOT a size bound (#2505): one very busy day, or a long-lived
30
+ // process that never re-runs the session-start sweep, grows a single
31
+ // day file without limit. The backup keeps the .jsonl extension so the
32
+ // retention sweep in log-cleanup.ts (which matches /\.jsonl$/ under
33
+ // logs/) reaps it like any other daily file.
34
+ const writer = createNdjsonLogger({
35
+ filePath: () => getLogFile(),
36
+ maxBytes: getMaxLogSizeMB() * 1024 * 1024,
37
+ backupPath: () => getLogFile().replace(/\.jsonl$/, ".rotated.jsonl"),
38
+ });
29
39
  return {
30
40
  log(entry) {
31
41
  if (isTestMode()) {
@@ -0,0 +1,39 @@
1
+ /** Observed-latency classification shared by dispatch and LSP tier seams. */
2
+ import { normalizeMapKey } from "../path-utils.js";
3
+ /** A runner gets this much time before the next run moves off the write path. */
4
+ export const COLLECT_LATER_THRESHOLD_MS = 5_000;
5
+ const slowRunners = new Map();
6
+ function key(projectRoot, runnerId) {
7
+ return `${normalizeMapKey(projectRoot)}\u0000${runnerId}`;
8
+ }
9
+ /** Read the current observed tier. Missing observations stay inline. */
10
+ export function classifyObservedRunner(projectRoot, runnerId) {
11
+ return slowRunners.has(key(projectRoot, runnerId))
12
+ ? "collect-later"
13
+ : "inline";
14
+ }
15
+ /** Record one completed latency observation and return the resulting tier. */
16
+ export function observeRunnerLatency(options) {
17
+ const normalizedProjectRoot = normalizeMapKey(options.projectRoot);
18
+ const runnerKey = key(normalizedProjectRoot, options.runnerId);
19
+ if (options.timedOut === true ||
20
+ options.durationMs > COLLECT_LATER_THRESHOLD_MS) {
21
+ slowRunners.set(runnerKey, {
22
+ projectRoot: normalizedProjectRoot,
23
+ runnerId: options.runnerId,
24
+ });
25
+ return "collect-later";
26
+ }
27
+ // A fast completed run is affirmative recovery, not merely absence of a
28
+ // failure. This deliberately prevents a process-lifetime latch.
29
+ slowRunners.delete(runnerKey);
30
+ return "inline";
31
+ }
32
+ /** Session boundary reset. */
33
+ export function resetObservedRunnerLatency() {
34
+ slowRunners.clear();
35
+ }
36
+ /** Test-only inspection without exposing mutable state. */
37
+ export function observedRunnerTierForTests(projectRoot, runnerId) {
38
+ return classifyObservedRunner(projectRoot, runnerId);
39
+ }