pi-lens 3.8.73 → 4.0.0

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 (346) hide show
  1. package/CHANGELOG.md +2614 -12
  2. package/README.md +50 -21
  3. package/dist/clients/actionable-warnings.js +73 -18
  4. package/dist/clients/agent-behavior-client.js +21 -4
  5. package/dist/clients/ast-grep-client.js +68 -16
  6. package/dist/clients/ast-grep-types.js +0 -1
  7. package/dist/clients/ast-grep-yaml-synth.js +37 -10
  8. package/dist/clients/atomic-write-staging.js +63 -0
  9. package/dist/clients/atomic-write.js +116 -16
  10. package/dist/clients/bash-file-access.js +193 -23
  11. package/dist/clients/biome-client.js +29 -25
  12. package/dist/clients/bootstrap.js +11 -2
  13. package/dist/clients/bounded-cache.js +34 -0
  14. package/dist/clients/bounded-pid-file-lock.js +192 -0
  15. package/dist/clients/bus-events-logger.js +5 -1
  16. package/dist/clients/bus-publish.js +10 -3
  17. package/dist/clients/cache/rule-cache.js +89 -6
  18. package/dist/clients/cache-manager.js +97 -9
  19. package/dist/clients/cache-observability.js +498 -0
  20. package/dist/clients/call-graph.js +460 -108
  21. package/dist/clients/cascade-logger.js +5 -1
  22. package/dist/clients/child-unref.js +85 -0
  23. package/dist/clients/codebase-model.js +52 -23
  24. package/dist/clients/collateral-test-role.js +40 -0
  25. package/dist/clients/complexity-client.js +6 -8
  26. package/dist/clients/console-guard-install.js +14 -0
  27. package/dist/clients/cooperative-budget.js +42 -0
  28. package/dist/clients/dead-code-client.js +2 -1
  29. package/dist/clients/debug-handles.js +206 -0
  30. package/dist/clients/debug-heap.js +167 -0
  31. package/dist/clients/degradation-ledger.js +153 -0
  32. package/dist/clients/dependency-checker.js +466 -63
  33. package/dist/clients/diagnostic-dispositions.js +83 -13
  34. package/dist/clients/diagnostics-publish.js +10 -3
  35. package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
  36. package/dist/clients/dispatch/dispatcher.js +71 -13
  37. package/dist/clients/dispatch/fact-store.js +11 -0
  38. package/dist/clients/dispatch/facts/function-facts.js +37 -7
  39. package/dist/clients/dispatch/facts/import-facts.js +108 -119
  40. package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
  41. package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
  42. package/dist/clients/dispatch/indent-detect.js +35 -0
  43. package/dist/clients/dispatch/inline-suppressions.js +17 -8
  44. package/dist/clients/dispatch/integration.js +1094 -611
  45. package/dist/clients/dispatch/lazy.js +14 -0
  46. package/dist/clients/dispatch/plan.js +11 -1
  47. package/dist/clients/dispatch/rule-id-normalize.js +50 -0
  48. package/dist/clients/dispatch/rule-policy.js +135 -0
  49. package/dist/clients/dispatch/rules/async-noise.js +1 -0
  50. package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
  51. package/dist/clients/dispatch/runners/ast-grep-napi.js +47 -6
  52. package/dist/clients/dispatch/runners/biome-check.js +5 -9
  53. package/dist/clients/dispatch/runners/detekt.js +3 -2
  54. package/dist/clients/dispatch/runners/hadolint.js +2 -1
  55. package/dist/clients/dispatch/runners/helm-lint.js +154 -0
  56. package/dist/clients/dispatch/runners/htmlhint.js +3 -2
  57. package/dist/clients/dispatch/runners/index.js +5 -1
  58. package/dist/clients/dispatch/runners/lsp.js +14 -6
  59. package/dist/clients/dispatch/runners/markdownlint.js +4 -9
  60. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
  61. package/dist/clients/dispatch/runners/pyright.js +9 -5
  62. package/dist/clients/dispatch/runners/ruff.js +4 -8
  63. package/dist/clients/dispatch/runners/shellcheck.js +2 -3
  64. package/dist/clients/dispatch/runners/shfmt.js +2 -3
  65. package/dist/clients/dispatch/runners/spotbugs.js +2 -1
  66. package/dist/clients/dispatch/runners/taplo.js +2 -1
  67. package/dist/clients/dispatch/runners/terragrunt.js +163 -0
  68. package/dist/clients/dispatch/runners/tflint.js +37 -5
  69. package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
  70. package/dist/clients/dispatch/runners/trivy-config.js +13 -7
  71. package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  72. package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
  73. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  74. package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  75. package/dist/clients/disposition-publish.js +10 -3
  76. package/dist/clients/durable-store.js +97 -0
  77. package/dist/clients/event-loop-monitor.js +94 -3
  78. package/dist/clients/extension-log.js +139 -0
  79. package/dist/clients/extension-mode.js +73 -0
  80. package/dist/clients/feature-hints.js +53 -7
  81. package/dist/clients/file-kinds.js +143 -31
  82. package/dist/clients/file-role.js +18 -3
  83. package/dist/clients/file-utils.js +56 -34
  84. package/dist/clients/fix-worklog.js +14 -2
  85. package/dist/clients/format-events-publish.js +16 -3
  86. package/dist/clients/format-service.js +24 -8
  87. package/dist/clients/formatters-lazy.js +8 -0
  88. package/dist/clients/formatters.js +358 -61
  89. package/dist/clients/generated-artifacts.js +169 -15
  90. package/dist/clients/git-guard.js +681 -11
  91. package/dist/clients/git-tracked-ignore.js +4 -0
  92. package/dist/clients/go-client.js +2 -1
  93. package/dist/clients/govulncheck-client.js +8 -0
  94. package/dist/clients/grammar-source.js +12 -4
  95. package/dist/clients/gzip-stage-write.js +102 -0
  96. package/dist/clients/host-ports.js +31 -0
  97. package/dist/clients/installer/index.js +733 -213
  98. package/dist/clients/instance-reaper.js +142 -172
  99. package/dist/clients/jscpd-client.js +31 -54
  100. package/dist/clients/knip-client.js +78 -36
  101. package/dist/clients/language-policy.js +14 -2
  102. package/dist/clients/language-profile.js +72 -77
  103. package/dist/clients/latency-logger.js +28 -16
  104. package/dist/clients/lens-config.js +140 -166
  105. package/dist/clients/lens-engine.js +124 -21
  106. package/dist/clients/lens-events.js +27 -4
  107. package/dist/clients/lens-flag-registry.js +293 -0
  108. package/dist/clients/lens-map.js +96 -26
  109. package/dist/clients/live-bus-emitter.js +30 -0
  110. package/dist/clients/log-cleanup.js +8 -18
  111. package/dist/clients/lsp/cascade-tier.js +23 -3
  112. package/dist/clients/lsp/client.js +566 -57
  113. package/dist/clients/lsp/config.js +74 -10
  114. package/dist/clients/lsp/diagnostic-binding.js +86 -0
  115. package/dist/clients/lsp/edits.js +1012 -121
  116. package/dist/clients/lsp/index.js +1840 -803
  117. package/dist/clients/lsp/language.js +4 -1
  118. package/dist/clients/lsp/launch.js +40 -32
  119. package/dist/clients/lsp/path-utils.js +1 -1
  120. package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  121. package/dist/clients/lsp/server.js +279 -60
  122. package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  123. package/dist/clients/lsp-document-symbols.js +112 -0
  124. package/dist/clients/lsp-lazy.js +8 -0
  125. package/dist/clients/lsp-mutation.js +248 -0
  126. package/dist/clients/mcp/analyze.js +128 -36
  127. package/dist/clients/mcp/ipc.js +157 -24
  128. package/dist/clients/mcp/review.js +36 -3
  129. package/dist/clients/mcp/session.js +216 -8
  130. package/dist/clients/memory-sampler.js +115 -0
  131. package/dist/clients/metrics-client.js +2 -1
  132. package/dist/clients/metrics-history.js +2 -1
  133. package/dist/clients/module-report-lsp.js +5 -0
  134. package/dist/clients/module-report.js +267 -82
  135. package/dist/clients/ndjson-logger.js +334 -103
  136. package/dist/clients/opengrep-client.js +1 -1
  137. package/dist/clients/partial-edit-apply.js +120 -5
  138. package/dist/clients/path-keyed-map.js +76 -0
  139. package/dist/clients/path-utils.js +141 -5
  140. package/dist/clients/performance-report.js +327 -0
  141. package/dist/clients/pipeline.js +54 -13
  142. package/dist/clients/print-mode.js +21 -0
  143. package/dist/clients/project-changes.js +117 -4
  144. package/dist/clients/project-diagnostics/cache.js +3 -2
  145. package/dist/clients/project-diagnostics/extractors.js +26 -115
  146. package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
  147. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +117 -0
  148. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
  149. package/dist/clients/project-diagnostics/scanner.js +268 -128
  150. package/dist/clients/project-lens-config.js +138 -34
  151. package/dist/clients/project-report.js +64 -25
  152. package/dist/clients/project-scale.js +8 -10
  153. package/dist/clients/project-snapshot-persist-worker.js +6 -0
  154. package/dist/clients/project-snapshot.js +767 -8
  155. package/dist/clients/project-trust.js +181 -0
  156. package/dist/clients/quiet-window.js +27 -5
  157. package/dist/clients/read-bridge.js +147 -0
  158. package/dist/clients/read-expansion.js +22 -12
  159. package/dist/clients/read-guard-logger.js +159 -1
  160. package/dist/clients/read-guard-tool-lines.js +150 -29
  161. package/dist/clients/read-guard.js +262 -3
  162. package/dist/clients/redact/secrets.js +449 -0
  163. package/dist/clients/resource-sampler.js +144 -39
  164. package/dist/clients/reverse-deps.js +48 -0
  165. package/dist/clients/review-graph/builder.js +2570 -326
  166. package/dist/clients/review-graph/git-identity.js +2 -1
  167. package/dist/clients/review-graph/import-resolvers.js +21 -10
  168. package/dist/clients/review-graph/persist-worker.js +10 -0
  169. package/dist/clients/review-graph/query.js +5 -0
  170. package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
  171. package/dist/clients/review-graph/symbol-id.js +46 -0
  172. package/dist/clients/review-graph/tsconfig-paths.js +57 -6
  173. package/dist/clients/review-graph/workspace-modules.js +51 -3
  174. package/dist/clients/review-graph-logger.js +53 -0
  175. package/dist/clients/ruff-client.js +27 -44
  176. package/dist/clients/runtime-agent-end.js +111 -75
  177. package/dist/clients/runtime-context.js +36 -12
  178. package/dist/clients/runtime-coordinator.js +76 -8
  179. package/dist/clients/runtime-session.js +799 -137
  180. package/dist/clients/runtime-tool-call.js +117 -62
  181. package/dist/clients/runtime-tool-result.js +218 -23
  182. package/dist/clients/runtime-turn.js +284 -40
  183. package/dist/clients/rust-client.js +2 -1
  184. package/dist/clients/safe-spawn.js +873 -71
  185. package/dist/clients/security-scan-client.js +2 -1
  186. package/dist/clients/session-start-observability.js +15 -0
  187. package/dist/clients/session-state-store.js +5 -3
  188. package/dist/clients/sessionstart-logger.js +18 -0
  189. package/dist/clients/sg-runner.js +284 -159
  190. package/dist/clients/slow-fs.js +2 -1
  191. package/dist/clients/smells-rollup.js +224 -0
  192. package/dist/clients/source-filter.js +231 -43
  193. package/dist/clients/source-walker.js +66 -10
  194. package/dist/clients/startup-scan.js +15 -16
  195. package/dist/clients/startup-timing.js +9 -0
  196. package/dist/clients/test-runner-client.js +38 -8
  197. package/dist/clients/todo-scanner.js +30 -3
  198. package/dist/clients/tool-event.js +35 -0
  199. package/dist/clients/tool-policy.js +388 -39
  200. package/dist/clients/tool-render.js +155 -0
  201. package/dist/clients/tree-sitter-cache.js +153 -140
  202. package/dist/clients/tree-sitter-client.js +1722 -80
  203. package/dist/clients/tree-sitter-logger.js +64 -1
  204. package/dist/clients/tree-sitter-query-loader.js +117 -12
  205. package/dist/clients/tree-sitter-shared.js +48 -19
  206. package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
  207. package/dist/clients/user-notify.js +51 -0
  208. package/dist/clients/vanished-instance-marker.js +57 -0
  209. package/dist/clients/warm-attach.js +33 -13
  210. package/dist/clients/widget-state.js +342 -49
  211. package/dist/clients/word-index-logger.js +44 -0
  212. package/dist/clients/word-index.js +612 -68
  213. package/dist/clients/workspace-topology.js +100 -7
  214. package/dist/index.js +65923 -47161
  215. package/dist/mcp/analyze-cli.js +185 -29
  216. package/dist/mcp/cli.js +110 -0
  217. package/dist/mcp/server.js +272 -50
  218. package/dist/tools/ast-grep-search.js +93 -27
  219. package/dist/tools/lens-diagnostics.js +247 -41
  220. package/dist/tools/lsp-diagnostics.js +153 -40
  221. package/dist/tools/lsp-navigation.js +51 -31
  222. package/dist/tools/module-report.js +9 -0
  223. package/dist/tools/symbol-search.js +21 -2
  224. package/docs/agent-guide.md +247 -0
  225. package/docs/agent-tools.md +21 -10
  226. package/docs/analysisall.md +192 -0
  227. package/docs/api-ports-inventory.md +165 -0
  228. package/docs/ast-grep_rules_catalog.md +5 -2
  229. package/docs/astplayground.md +3 -4
  230. package/docs/audit1.md +5 -5
  231. package/docs/durable-store-audit-1202.md +32 -0
  232. package/docs/environment-variables.md +197 -11
  233. package/docs/fable.md +27 -0
  234. package/docs/features.md +35 -5
  235. package/docs/globalconfig.md +101 -5
  236. package/docs/language-coverage.md +6 -5
  237. package/docs/lsp-capability-matrix.md +2 -2
  238. package/docs/mcp.md +54 -20
  239. package/docs/servercapabilities.md +7 -4
  240. package/docs/settings.md +192 -0
  241. package/docs/tools.md +18 -1
  242. package/docs/tree-sitter_rules_catalog.md +11 -31
  243. package/docs/usage.md +31 -0
  244. package/package.json +139 -138
  245. package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
  246. package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
  247. package/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  248. package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  249. package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
  250. package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
  251. package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
  252. package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
  253. package/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  254. package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  255. package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  256. package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
  257. package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
  258. package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
  259. package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
  260. package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
  261. package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
  262. package/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  263. package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  264. package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
  265. package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
  266. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
  267. package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
  268. package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
  269. package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
  270. package/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  271. package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  272. package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  273. package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
  274. package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
  275. package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
  276. package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
  277. package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
  278. package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
  279. package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
  280. package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
  281. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
  282. package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
  283. package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
  284. package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
  285. package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
  286. package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
  287. package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
  288. package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
  289. package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
  290. package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
  291. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
  292. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
  293. package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
  294. package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
  295. package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
  296. package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
  297. package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
  298. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
  299. package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
  300. package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
  301. package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
  302. package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
  303. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
  304. package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
  305. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
  306. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
  307. package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
  308. package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
  309. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
  310. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
  311. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  312. package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
  313. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
  314. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
  315. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
  316. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
  317. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
  318. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
  319. package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
  320. package/rules/typos/_typos.toml +17 -0
  321. package/scripts/analyze-pi-lens-logs.mjs +55 -2
  322. package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
  323. package/dist/clients/ast-grep-parser.js +0 -86
  324. package/dist/clients/config-enabled-shape.js +0 -39
  325. package/dist/clients/lsp/interactive-install.js +0 -367
  326. package/dist/clients/lsp/lsp-index.js +0 -10
  327. package/dist/clients/lsp/server-strategies.js +0 -5
  328. package/dist/clients/project-metadata.js +0 -690
  329. package/dist/clients/source-groups.js +0 -96
  330. package/dist/clients/types.js +0 -11
  331. package/docs/env_variables.md +0 -10
  332. package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
  333. package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
  334. package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
  335. package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
  336. package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
  337. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
  338. package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
  339. package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
  340. package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
  341. package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
  342. package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
  343. package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
  344. package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
  345. package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
  346. package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
@@ -0,0 +1,76 @@
1
+ /**
2
+ * A `Map` for file-path keys that folds every key through a caller-supplied
3
+ * normalizer INTERNALLY, so keying on a raw (non-normalized) path is
4
+ * structurally impossible — the whole path-key divergence class (#1025,
5
+ * follow-up to #1020/#210) collapses to "pick the right normalizer once, at
6
+ * construction," instead of every call site having to remember to normalize on
7
+ * write AND read AND delete (the exact discipline #210 declared and #1020
8
+ * proved is easy to miss).
9
+ *
10
+ * The normalizer runs on EVERY `get`/`set`/`has`/`delete`, so two
11
+ * differently-formed keys for the same path (`SUB\a.ts` vs `sub/a.ts`, on a
12
+ * case-insensitive FS) collapse to a single entry — no silent duplicate, no
13
+ * `has()` miss, no orphaned stale entry.
14
+ *
15
+ * Values retain their ORIGINAL display path. Consumers that render paths
16
+ * (symbol_search results, footer widgets) must not see a lowercased /
17
+ * slash-folded key, so the entry stores the display path alongside the value
18
+ * and every iteration surface (`keys`/`values`/`entries`/`forEach`/the default
19
+ * iterator) yields that display path — the exact form last written, never the
20
+ * folded internal key. `set` refreshes the display path to the most recent
21
+ * write (last-writer-wins), matching a plain `Map`'s "the key you set is the
22
+ * key you see" intuition while still de-duplicating by normalized identity.
23
+ *
24
+ * Choose the normalizer to match how the keyed state is used:
25
+ * - `normalizeEphemeralMapKey` (path-utils.ts) — cheap slash-fold +
26
+ * win32-lowercase, NO filesystem I/O — for hot, single-process, in-memory
27
+ * indexes whose keys this process produced itself (e.g. the word index).
28
+ * - `normalizeMapKey` — realpath-canonicalizing — for long-lived state shared
29
+ * across call sites where symlink/real-casing resolution matters.
30
+ */
31
+ export class PathKeyedMap {
32
+ normalize;
33
+ store = new Map();
34
+ constructor(normalize) {
35
+ this.normalize = normalize;
36
+ }
37
+ get size() {
38
+ return this.store.size;
39
+ }
40
+ get(path) {
41
+ return this.store.get(this.normalize(path))?.value;
42
+ }
43
+ has(path) {
44
+ return this.store.has(this.normalize(path));
45
+ }
46
+ set(path, value) {
47
+ this.store.set(this.normalize(path), { displayPath: path, value });
48
+ return this;
49
+ }
50
+ delete(path) {
51
+ return this.store.delete(this.normalize(path));
52
+ }
53
+ clear() {
54
+ this.store.clear();
55
+ }
56
+ *keys() {
57
+ for (const entry of this.store.values())
58
+ yield entry.displayPath;
59
+ }
60
+ *values() {
61
+ for (const entry of this.store.values())
62
+ yield entry.value;
63
+ }
64
+ *entries() {
65
+ for (const entry of this.store.values())
66
+ yield [entry.displayPath, entry.value];
67
+ }
68
+ forEach(callback) {
69
+ for (const entry of this.store.values()) {
70
+ callback(entry.value, entry.displayPath, this);
71
+ }
72
+ }
73
+ [Symbol.iterator]() {
74
+ return this.entries();
75
+ }
76
+ }
@@ -13,13 +13,63 @@
13
13
  import { existsSync, realpathSync } from "node:fs";
14
14
  import * as os from "node:os";
15
15
  import * as path from "node:path";
16
- import { dirname, win32 } from "node:path";
16
+ import { win32 } from "node:path";
17
17
  import { fileURLToPath, pathToFileURL } from "node:url";
18
+ import { minimatch } from "./deps/minimatch.js";
18
19
  /**
19
- * Detect if a path is a Windows path (has drive letter or UNC prefix).
20
+ * Detect a positively Windows-shaped path, regardless of the host OS.
21
+ *
22
+ * A backslash anywhere in a path is not enough: it is a legal character in a
23
+ * POSIX filename. Only a drive-letter prefix (`X:`), a UNC root (`\\`), or a
24
+ * rooted backslash at position zero (`\`) selects Windows parsing.
25
+ */
26
+ export function isWindowsPath(filePath) {
27
+ return /^[A-Za-z]:/.test(filePath) || filePath.startsWith("\\");
28
+ }
29
+ /**
30
+ * Canonical backslash→forward-slash fold — the single sanctioned form of the
31
+ * `p.replace(/\\/g, "/")` idiom otherwise hand-rolled across the codebase
32
+ * (~138 sites, #1193). PURE separator normalization: it does NOT resolve,
33
+ * canonicalize, lowercase, or collapse repeated slashes — reach for
34
+ * `normalizeFilePath`/`normalizeMapKey`/`normalizeEphemeralMapKey` when a
35
+ * canonical map *key* (case-fold / realpath) is what you need. Consolidating on
36
+ * this funnels the scattered transform and makes a shape-2 lint/ast-grep rule
37
+ * possible for the first time: today a bare inline `.replace(/\\/g, "/")` is
38
+ * byte-identical to the sanctioned use so it can't be ruled (#1158); once
39
+ * everything routes through `toPosix`, an *un-migrated* inline `.replace`
40
+ * becomes detectable.
41
+ */
42
+ export function toPosix(filePath) {
43
+ return filePath.replace(/\\/g, "/");
44
+ }
45
+ /** Return whether `filePath` is fully qualified under Windows semantics. */
46
+ export function isFullyQualifiedWin32(filePath) {
47
+ return win32.isAbsolute(filePath) && win32.parse(filePath).root.length > 1;
48
+ }
49
+ /** Return whether `filePath` is fully qualified under POSIX semantics. */
50
+ export function isFullyQualifiedPosix(filePath) {
51
+ return path.posix.isAbsolute(filePath) && !isWindowsPath(filePath);
52
+ }
53
+ /**
54
+ * Return whether `filePath` is fully qualified under the host's semantics.
55
+ *
56
+ * In particular, `/foo` is rooted-relative under Win32 (ambient-drive
57
+ * dependent) but fully qualified under POSIX.
58
+ */
59
+ export function isFullyQualified(filePath) {
60
+ return process.platform === "win32"
61
+ ? isFullyQualifiedWin32(filePath)
62
+ : isFullyQualifiedPosix(filePath);
63
+ }
64
+ /**
65
+ * Split a path into its non-empty segments on EITHER separator (`\` or `/`),
66
+ * regardless of the running OS — the shape-safe form of `p.split(path.sep)` /
67
+ * an inline `p.split(/[\\/]+/)`, which #1161/#1163 showed must not assume the
68
+ * host separator for a possibly-cross-shaped path. Drops empty segments
69
+ * (leading slash, drive-root, doubled separators).
20
70
  */
21
- function isWindowsPath(filePath) {
22
- return /^[A-Za-z]:/.test(filePath) || filePath.startsWith("\\\\");
71
+ export function splitPathSegments(filePath) {
72
+ return filePath.split(/[\\/]+/).filter(Boolean);
23
73
  }
24
74
  /**
25
75
  * Normalize a file path for consistent Map key usage.
@@ -86,7 +136,15 @@ function resolveNonExisting(filePath) {
86
136
  const base = canonical.replace(/\\/g, "/");
87
137
  return base.endsWith("/") ? base + tail : `${base}/${tail}`;
88
138
  }
89
- const parent = dirname(current);
139
+ // Use win32.dirname (not the platform-default dirname) so a
140
+ // Windows-shaped path is parsed with win32 semantics regardless of the
141
+ // running OS — consistent with the win32.resolve/win32.normalize this
142
+ // branch already commits to. The platform-default POSIX dirname would
143
+ // find no separator in a win32-resolved "C:\repo\..." path (its only
144
+ // separators are backslashes), collapse to ".", stop the upward walk at
145
+ // cwd, and mangle the key on Linux CI (refs #1150, the #1024
146
+ // OS-divergence class).
147
+ const parent = win32.dirname(current);
90
148
  if (parent === current) {
91
149
  // Reached filesystem root without finding existing dir
92
150
  // Fall back to full lowercase
@@ -110,6 +168,29 @@ export function uriToPath(uri) {
110
168
  return normalizeFilePath(uri);
111
169
  }
112
170
  }
171
+ /**
172
+ * Decode a file:// URI to an on-disk path WITHOUT map-key normalization.
173
+ *
174
+ * `uriToPath` runs its result through `normalizeFilePath`, which on win32
175
+ * lowercases the nonexistent tail of a path (see `resolveNonExisting`) and
176
+ * canonicalizes an existing path to its real casing. That is correct for Map
177
+ * keys, but DESTRUCTIVE for a real create/rename target: creating `NewFile.txt`
178
+ * would write `newfile.txt`, and a legitimate case-only rename would collapse
179
+ * to a no-op ("source and destination must differ"). Disk mutations must honor
180
+ * the caller's intended casing, so they resolve their target through this
181
+ * decode-only path while confinement/validation keep using the normalized
182
+ * `uriToPath`. Non-win32 is unaffected either way (normalizeFilePath is a
183
+ * near-identity there).
184
+ */
185
+ export function uriToDiskPath(uri) {
186
+ try {
187
+ return fileURLToPath(uri);
188
+ }
189
+ catch {
190
+ // Not a valid file:// URI — treat as a plain path (matches uriToPath).
191
+ return uri;
192
+ }
193
+ }
113
194
  /**
114
195
  * Convert a path to a file:// URI.
115
196
  * Does NOT normalize the path - URIs preserve original casing.
@@ -124,6 +205,31 @@ export function pathToUri(filePath) {
124
205
  export function normalizeMapKey(filePath) {
125
206
  return normalizeFilePath(filePath);
126
207
  }
208
+ /**
209
+ * Human-facing path relative to a project root when the file is inside it.
210
+ *
211
+ * Parses by path SHAPE, not host OS (refs #1150/#1152, shape-2 class #1163):
212
+ * a Windows-shaped `filePath` (drive-letter/UNC — e.g. a persisted call-graph
213
+ * symbol-key path `C:\repo\src\x.ts` rehydrated on a Linux CI run) is split
214
+ * with `win32.*` regardless of `process.platform`. The host-default
215
+ * `isAbsolute`/`relative` find no drive-letter anchor in a win32 path on POSIX:
216
+ * `path.isAbsolute("C:\\repo\\x.ts")` returns FALSE on Linux, short-circuiting
217
+ * to the raw absolute path instead of ever relativizing it — so a file that IS
218
+ * under the project root renders as a full absolute path on Linux but the
219
+ * expected `src/x.ts` on Windows (green-locally / wrong-on-CI, the #1024
220
+ * divergence class). `win32.*` on a native POSIX path (Windows never sees one;
221
+ * Linux native paths aren't Windows-shaped) is never selected, so same-OS
222
+ * native paths are unchanged either way.
223
+ */
224
+ export function toProjectRelativePath(filePath, projectRoot) {
225
+ const p = isWindowsPath(filePath) ? win32 : path;
226
+ if (!p.isAbsolute(filePath))
227
+ return filePath.replace(/\\/g, "/");
228
+ const relative = p.relative(p.resolve(projectRoot), filePath);
229
+ return relative && !relative.startsWith("..") && !p.isAbsolute(relative)
230
+ ? relative.replace(/\\/g, "/")
231
+ : filePath.replace(/\\/g, "/");
232
+ }
127
233
  /**
128
234
  * Cheap, syntactic-only Map key normalization: slash-fold + (on Windows)
129
235
  * lowercase. No `realpathSync` / filesystem I/O.
@@ -146,6 +252,11 @@ export function normalizeMapKey(filePath) {
146
252
  * real-casing resolution actually matters.
147
253
  */
148
254
  export function normalizeEphemeralMapKey(filePath) {
255
+ // Most hot-path keys on POSIX are already canonical slash-separated strings.
256
+ // Preserve that identity instead of allocating a replacement string for each
257
+ // file in a large diagnostics reconciliation.
258
+ if (process.platform !== "win32" && !filePath.includes("\\"))
259
+ return filePath;
149
260
  const slashed = filePath.replace(/\\/g, "/");
150
261
  return process.platform === "win32" ? slashed.toLowerCase() : slashed;
151
262
  }
@@ -316,3 +427,28 @@ export function isExternalOrVendorFile(filePath, projectRoot) {
316
427
  : normalized;
317
428
  return rel.split("/").some((seg) => VENDOR_DIR_NAMES.has(seg));
318
429
  }
430
+ /**
431
+ * Shared marker-glob semantics for every "does this directory contain a file
432
+ * matching this glob" probe (#895 review): match against the entry NAME only,
433
+ * `dot: true` so dotfile markers match, `nocase` on win32 to match the
434
+ * filesystem (and the project ignore matcher). The three marker probes —
435
+ * language-profile.ts `hasProjectMarker`, workspace-topology.ts
436
+ * `hasBasenameMarker`, lsp/server.ts `markerExists` — must all route their
437
+ * glob matching through here rather than call minimatch with hand-copied
438
+ * options.
439
+ */
440
+ export function nameMatchesMarkerGlob(name, pattern) {
441
+ return minimatch(name, pattern, {
442
+ dot: true,
443
+ nocase: process.platform === "win32",
444
+ });
445
+ }
446
+ /**
447
+ * Files/symlinks-only marker-glob probe over a directory listing — a
448
+ * *directory* named like a marker (e.g. a `Foo.csproj/` dir) is not a project
449
+ * file (#201).
450
+ */
451
+ export function direntsHaveMarkerGlobMatch(entries, pattern) {
452
+ return entries.some((entry) => (entry.isFile() || entry.isSymbolicLink()) &&
453
+ nameMatchesMarkerGlob(entry.name, pattern));
454
+ }
@@ -0,0 +1,327 @@
1
+ import * as fs from "node:fs";
2
+ import * as readline from "node:readline";
3
+ import { flushLatencyLog, getLatencyLogPath, } from "./latency-logger.js";
4
+ import { getMaxLogSizeMB } from "./log-cleanup.js";
5
+ export const DEFAULT_PERF_TOP_N = 5;
6
+ export const MAX_PERF_TOP_N = 50;
7
+ export const MAX_PERF_PHASE_SAMPLES = 20_000;
8
+ const PARSE_YIELD_EVERY = 500;
9
+ // The window follows the same threshold that rotates the log out from under it.
10
+ // Exported so a test can size a fixture to the window it means to saturate
11
+ // rather than to a second copy of the default that can drift out of agreement.
12
+ export function resolveLogByteBudget() {
13
+ return getMaxLogSizeMB() * 1024 * 1024;
14
+ }
15
+ function boundedPositiveInteger(value, maximum, fallback = maximum) {
16
+ return Number.isFinite(value) && value > 0
17
+ ? Math.min(maximum, Math.max(1, Math.floor(value)))
18
+ : fallback;
19
+ }
20
+ function percentile(sorted, quantile) {
21
+ const position = (sorted.length - 1) * quantile;
22
+ const lower = Math.floor(position);
23
+ const upper = Math.ceil(position);
24
+ return lower === upper
25
+ ? sorted[lower]
26
+ : sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
27
+ }
28
+ function roundedMs(value) {
29
+ return Math.round(value * 10) / 10;
30
+ }
31
+ function isEntryObject(value) {
32
+ return !!value && typeof value === "object" && !Array.isArray(value);
33
+ }
34
+ function isPhaseRecord(value) {
35
+ if (!isEntryObject(value))
36
+ return false;
37
+ return (value.type === "phase" &&
38
+ typeof value.phase === "string" &&
39
+ value.phase.trim().length > 0 &&
40
+ typeof value.durationMs === "number" &&
41
+ Number.isFinite(value.durationMs) &&
42
+ value.durationMs >= 0 &&
43
+ (value.toolName === undefined || typeof value.toolName === "string") &&
44
+ (value.ts === undefined || typeof value.ts === "string") &&
45
+ (value.startedAt === undefined || typeof value.startedAt === "string") &&
46
+ (value.pid === undefined ||
47
+ (typeof value.pid === "number" && Number.isFinite(value.pid))));
48
+ }
49
+ function isPhaseSample(value) {
50
+ return isPhaseRecord(value) && value.durationMs > 0;
51
+ }
52
+ function phaseKey(entry) {
53
+ const phase = entry.phase?.trim() ?? "";
54
+ const toolName = entry.toolName?.trim();
55
+ return toolName ? `${toolName}/${phase}` : phase;
56
+ }
57
+ function isCurrentSessionSample(entry, processId, sessionStartedAt) {
58
+ if (entry.pid !== processId || typeof entry.ts !== "string")
59
+ return false;
60
+ const finishedAt = Date.parse(entry.ts);
61
+ if (!Number.isFinite(finishedAt) || finishedAt < sessionStartedAt)
62
+ return false;
63
+ if (entry.startedAt === undefined)
64
+ return true;
65
+ const startedAt = Date.parse(entry.startedAt);
66
+ return Number.isFinite(startedAt) && startedAt >= sessionStartedAt;
67
+ }
68
+ function createPhaseBuffer(limit) {
69
+ return { values: [], nextIndex: 0, total: 0, limit };
70
+ }
71
+ function retainEntry(buffer, entry) {
72
+ buffer.total += 1;
73
+ if (buffer.values.length < buffer.limit) {
74
+ buffer.values.push(entry);
75
+ return;
76
+ }
77
+ buffer.values[buffer.nextIndex] = entry;
78
+ buffer.nextIndex = (buffer.nextIndex + 1) % buffer.limit;
79
+ }
80
+ function orderedEntries(buffer) {
81
+ return buffer.total > buffer.values.length
82
+ ? [
83
+ ...buffer.values.slice(buffer.nextIndex),
84
+ ...buffer.values.slice(0, buffer.nextIndex),
85
+ ]
86
+ : buffer.values;
87
+ }
88
+ export function summarizePhaseLatency(entries, topN = DEFAULT_PERF_TOP_N) {
89
+ const durationsByPhase = new Map();
90
+ let oldestTimestamp;
91
+ let newestTimestamp;
92
+ let sampleCount = 0;
93
+ for (const entry of entries) {
94
+ if (!isPhaseSample(entry))
95
+ continue;
96
+ const phase = phaseKey(entry);
97
+ const durations = durationsByPhase.get(phase) ?? [];
98
+ durations.push(entry.durationMs);
99
+ durationsByPhase.set(phase, durations);
100
+ sampleCount += 1;
101
+ if (typeof entry.ts === "string") {
102
+ const ms = Date.parse(entry.ts);
103
+ if (Number.isFinite(ms)) {
104
+ if (!oldestTimestamp || ms < oldestTimestamp.ms) {
105
+ oldestTimestamp = { ms, iso: entry.ts };
106
+ }
107
+ if (!newestTimestamp || ms > newestTimestamp.ms) {
108
+ newestTimestamp = { ms, iso: entry.ts };
109
+ }
110
+ }
111
+ }
112
+ }
113
+ const summaries = Array.from(durationsByPhase, ([phase, durations]) => {
114
+ const sorted = [...durations].sort((a, b) => a - b);
115
+ const p50RawMs = percentile(sorted, 0.5);
116
+ const p99RawMs = percentile(sorted, 0.99);
117
+ return {
118
+ phase,
119
+ samples: sorted.length,
120
+ p50Ms: roundedMs(p50RawMs),
121
+ p99Ms: roundedMs(p99RawMs),
122
+ p50RawMs,
123
+ p99RawMs,
124
+ };
125
+ });
126
+ const limit = boundedPositiveInteger(topN, MAX_PERF_TOP_N, DEFAULT_PERF_TOP_N);
127
+ const tieBreak = (a, b) => b.samples - a.samples || a.phase.localeCompare(b.phase);
128
+ const toPublic = ({ phase, samples, p50Ms, p99Ms, }) => ({
129
+ phase,
130
+ samples,
131
+ p50Ms,
132
+ p99Ms,
133
+ });
134
+ const rankBy = (primary, secondary) => [...summaries]
135
+ .sort((a, b) => b[primary] - a[primary] ||
136
+ b[secondary] - a[secondary] ||
137
+ tieBreak(a, b))
138
+ .slice(0, limit)
139
+ .map(toPublic);
140
+ return {
141
+ sampleCount,
142
+ phaseCount: durationsByPhase.size,
143
+ oldestTs: oldestTimestamp?.iso,
144
+ newestTs: newestTimestamp?.iso,
145
+ slowestByP50: rankBy("p50RawMs", "p99RawMs"),
146
+ slowestByP99: rankBy("p99RawMs", "p50RawMs"),
147
+ };
148
+ }
149
+ async function readPhaseLogTail(filePath, windowBytes, maxSamples, processId, sessionStartedAt) {
150
+ const empty = () => ({
151
+ logEntries: [],
152
+ sessionEntries: [],
153
+ windowTruncated: false,
154
+ logSamplesTruncated: false,
155
+ sessionSamplesTruncated: false,
156
+ totalPhaseSamples: 0,
157
+ totalSessionPhaseSamples: 0,
158
+ malformedLines: 0,
159
+ invalidRecords: 0,
160
+ });
161
+ let handle;
162
+ try {
163
+ handle = await fs.promises.open(filePath, "r");
164
+ const stat = await handle.stat();
165
+ if (!stat.isFile()) {
166
+ // POSIX rejects opening a directory with EISDIR, but Windows opens it
167
+ // successfully with size 0 — which would fall into the empty() branch
168
+ // and conflate "unreadable path" with "no data yet". Throw the same
169
+ // error POSIX would so both platforms surface the bad path.
170
+ const notAFile = new Error(`EISDIR: illegal operation on a directory, read '${filePath}'`);
171
+ notAFile.code = "EISDIR";
172
+ throw notAFile;
173
+ }
174
+ if (stat.size === 0)
175
+ return empty();
176
+ const sampleLimit = boundedPositiveInteger(maxSamples, MAX_PERF_PHASE_SAMPLES);
177
+ const start = Math.max(0, stat.size - windowBytes);
178
+ let discardPartialFirstLine = false;
179
+ if (start > 0) {
180
+ const previousByte = Buffer.alloc(1);
181
+ const { bytesRead } = await handle.read(previousByte, 0, 1, start - 1);
182
+ discardPartialFirstLine = bytesRead === 1 && previousByte[0] !== 0x0a;
183
+ }
184
+ const stream = handle.createReadStream({
185
+ start,
186
+ end: stat.size - 1,
187
+ autoClose: false,
188
+ encoding: "utf8",
189
+ });
190
+ const lines = readline.createInterface({
191
+ input: stream,
192
+ crlfDelay: Infinity,
193
+ });
194
+ const logBuffer = createPhaseBuffer(sampleLimit);
195
+ const sessionBuffer = createPhaseBuffer(sampleLimit);
196
+ let malformedLines = 0;
197
+ let invalidRecords = 0;
198
+ let lineCount = 0;
199
+ for await (const line of lines) {
200
+ lineCount += 1;
201
+ if (lineCount % PARSE_YIELD_EVERY === 0) {
202
+ await new Promise((resolve) => setImmediate(resolve));
203
+ }
204
+ if (discardPartialFirstLine) {
205
+ discardPartialFirstLine = false;
206
+ continue;
207
+ }
208
+ const trimmed = line.trim();
209
+ if (!trimmed)
210
+ continue;
211
+ try {
212
+ const value = JSON.parse(trimmed);
213
+ if (isPhaseSample(value)) {
214
+ retainEntry(logBuffer, value);
215
+ if (isCurrentSessionSample(value, processId, sessionStartedAt)) {
216
+ retainEntry(sessionBuffer, value);
217
+ }
218
+ }
219
+ else if (!isPhaseRecord(value) &&
220
+ (!isEntryObject(value) || value.type === "phase")) {
221
+ invalidRecords += 1;
222
+ }
223
+ }
224
+ catch {
225
+ malformedLines += 1;
226
+ }
227
+ }
228
+ return {
229
+ logEntries: orderedEntries(logBuffer),
230
+ sessionEntries: orderedEntries(sessionBuffer),
231
+ windowTruncated: start > 0,
232
+ logSamplesTruncated: logBuffer.total > sampleLimit,
233
+ sessionSamplesTruncated: sessionBuffer.total > sampleLimit,
234
+ totalPhaseSamples: logBuffer.total,
235
+ totalSessionPhaseSamples: sessionBuffer.total,
236
+ malformedLines,
237
+ invalidRecords,
238
+ };
239
+ }
240
+ catch (error) {
241
+ if (error.code === "ENOENT")
242
+ return empty();
243
+ throw error;
244
+ }
245
+ finally {
246
+ await handle?.close().catch(() => { });
247
+ }
248
+ }
249
+ export async function collectLatencyPerformance(options) {
250
+ await flushLatencyLog();
251
+ const logPath = options.logPath ?? getLatencyLogPath();
252
+ const processId = options.processId ?? process.pid;
253
+ const topN = boundedPositiveInteger(options.topN ?? DEFAULT_PERF_TOP_N, MAX_PERF_TOP_N, DEFAULT_PERF_TOP_N);
254
+ const byteBudget = resolveLogByteBudget();
255
+ const windowBytes = boundedPositiveInteger(options.maxBytes ?? byteBudget, byteBudget);
256
+ const tail = await readPhaseLogTail(logPath, windowBytes, options.maxSamples ?? MAX_PERF_PHASE_SAMPLES, processId, options.sessionStartedAt);
257
+ const { logEntries, sessionEntries, ...counters } = tail;
258
+ return {
259
+ logPath,
260
+ topN,
261
+ windowBytes,
262
+ ...counters,
263
+ session: summarizePhaseLatency(sessionEntries, topN),
264
+ logWindow: summarizePhaseLatency(logEntries, topN),
265
+ };
266
+ }
267
+ function formatDuration(ms) {
268
+ if (ms >= 1000)
269
+ return `${(ms / 1000).toFixed(2)}s`;
270
+ if (ms > 0 && ms < 1)
271
+ return `${ms.toFixed(2)}ms`;
272
+ return `${Number.isInteger(ms) ? ms : ms.toFixed(1)}ms`;
273
+ }
274
+ function formatByteBudget(bytes) {
275
+ if (bytes >= 1024 * 1024) {
276
+ return `${Number((bytes / (1024 * 1024)).toFixed(1))}MB`;
277
+ }
278
+ if (bytes >= 1024)
279
+ return `${Number((bytes / 1024).toFixed(1))}KB`;
280
+ return `${bytes}B`;
281
+ }
282
+ function formatTimestamp(ts) {
283
+ const parsed = new Date(ts);
284
+ return Number.isFinite(parsed.getTime())
285
+ ? `${parsed.toISOString().slice(0, 19).replace("T", " ")}Z`
286
+ : ts;
287
+ }
288
+ function renderRanking(lines, label, phases) {
289
+ lines.push(` ${label}:`);
290
+ for (const phase of phases) {
291
+ lines.push(` ${phase.phase}: p50 ${formatDuration(phase.p50Ms)}, p99 ${formatDuration(phase.p99Ms)}, n=${phase.samples}`);
292
+ }
293
+ }
294
+ function renderScope(lines, label, scope) {
295
+ const range = scope.oldestTs && scope.newestTs
296
+ ? `, ${formatTimestamp(scope.oldestTs)} to ${formatTimestamp(scope.newestTs)}`
297
+ : "";
298
+ lines.push("", `${label} (${scope.sampleCount} samples across ${scope.phaseCount} phases${range})`);
299
+ if (scope.slowestByP99.length === 0) {
300
+ lines.push(" No phase timings yet.");
301
+ return;
302
+ }
303
+ renderRanking(lines, "Highest p50", scope.slowestByP50);
304
+ renderRanking(lines, "Highest p99", scope.slowestByP99);
305
+ }
306
+ export function renderLatencyPerformanceReport(report) {
307
+ const lines = [
308
+ "⏱️ PI-LENS PERFORMANCE",
309
+ `Top ${report.topN} sustained and tail latency phases.`,
310
+ ];
311
+ renderScope(lines, "Current process session", report.session);
312
+ renderScope(lines, "Machine-wide active log window", report.logWindow);
313
+ lines.push("", `Source: ${report.logPath}`);
314
+ if (report.windowTruncated) {
315
+ lines.push(`Both scopes use only the newest ${formatByteBudget(report.windowBytes)} of the active log.`);
316
+ }
317
+ if (report.sessionSamplesTruncated) {
318
+ lines.push(`Session percentiles use the newest ${report.session.sampleCount.toLocaleString("en-US")} of ${report.totalSessionPhaseSamples.toLocaleString("en-US")} phase samples.`);
319
+ }
320
+ if (report.logSamplesTruncated) {
321
+ lines.push(`Machine-wide percentiles use the newest ${report.logWindow.sampleCount.toLocaleString("en-US")} of ${report.totalPhaseSamples.toLocaleString("en-US")} phase samples.`);
322
+ }
323
+ if (report.malformedLines > 0 || report.invalidRecords > 0) {
324
+ lines.push(`Skipped ${report.malformedLines} malformed line(s) and ${report.invalidRecords} invalid record(s).`);
325
+ }
326
+ return lines.join("\n");
327
+ }