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
@@ -9,19 +9,105 @@
9
9
  *
10
10
  * Errors are swallowed best-effort, matching every current logger. A
11
11
  * best-effort SYNC flush is registered on `process.on("exit")` (appendFileSync
12
- * is fine at exit — not the hot path; no child spawning, #234).
12
+ * is fine at exit — not the hot path; no child spawning, #234). Normal drains
13
+ * use promise-based mkdir/append/truncate, while rotation stays synchronous
14
+ * inside the already-deferred drain so it cannot race a flushSync rename. A
15
+ * The shared writer state uses the `NDJSON_GLOBAL_STATE_SCHEMA` protocol:
16
+ * version 1 (the 6a8a0994 shape) is upgraded in place to current version 2,
17
+ * retaining its queues while replacing stale exit flushers. A pre-7e4b9120
18
+ * private-queue shape is fenced rather than adopted, because its queues cannot
19
+ * be migrated safely.
13
20
  */
14
21
  import * as fs from "node:fs";
15
22
  import * as path from "node:path";
23
+ import { normalizeFilePath } from "./path-utils.js";
24
+ import { redactSecrets } from "./redact/secrets.js";
16
25
  function resolve(v) {
17
26
  return typeof v === "function" ? v() : v;
18
27
  }
19
- // One shared exit handler flushes every logger — avoids an EventEmitter
20
- // MaxListeners warning once more than ~10 loggers exist (we ship eight, plus
21
- // diagnostic + test instances). No child spawning at teardown (#234).
22
- const exitFlushers = new Set();
23
- let exitHandlerRegistered = false;
24
- /** Test-only view of the registered exit flushers (see ndjson-logger.test.ts). */
28
+ function runBestEffort(operation) {
29
+ try {
30
+ operation();
31
+ }
32
+ catch {
33
+ return;
34
+ }
35
+ }
36
+ const NDJSON_GLOBAL_STATE_SCHEMA = "pi-lens.ndjson-logger.state";
37
+ const NDJSON_LEGACY_GLOBAL_STATE_VERSION = 1;
38
+ const NDJSON_GLOBAL_STATE_VERSION = 2;
39
+ const NDJSON_GLOBAL_STATE_KEY = Symbol.for("pi-lens.ndjson-logger.state");
40
+ const globalStateHost = globalThis;
41
+ const existingGlobalState = globalStateHost[NDJSON_GLOBAL_STATE_KEY];
42
+ function isSharedWriterState(value) {
43
+ if (!value || typeof value !== "object")
44
+ return false;
45
+ const candidate = value;
46
+ const knownSchema = candidate.schema === undefined ||
47
+ candidate.schema === NDJSON_GLOBAL_STATE_SCHEMA;
48
+ const knownVersion = candidate.version === undefined ||
49
+ candidate.version === NDJSON_LEGACY_GLOBAL_STATE_VERSION ||
50
+ candidate.version === NDJSON_GLOBAL_STATE_VERSION;
51
+ return (knownSchema &&
52
+ knownVersion &&
53
+ candidate.writers instanceof Map &&
54
+ candidate.exitFlushers instanceof Set &&
55
+ candidate.registeredLogFiles instanceof Set &&
56
+ typeof candidate.exitHandlerRegistered === "boolean");
57
+ }
58
+ function isLegacyGlobalState(value) {
59
+ if (!value || typeof value !== "object")
60
+ return false;
61
+ const candidate = value;
62
+ return (candidate.exitFlushers instanceof Set &&
63
+ candidate.registeredLogFiles instanceof Set &&
64
+ typeof candidate.exitHandlerRegistered === "boolean");
65
+ }
66
+ let ndjsonGlobalState;
67
+ let legacyGlobalState;
68
+ if (isSharedWriterState(existingGlobalState)) {
69
+ // The 7e4b9120 graph had no version marker and 6a8a0994 used version 1.
70
+ // Both shapes are bridgeable: retain their queues, but replace every stale
71
+ // exit flusher with a closure from this module graph. A current state is
72
+ // left untouched so it keeps exactly one canonical flusher per writer.
73
+ const needsFlusherMigration = existingGlobalState.schema !== NDJSON_GLOBAL_STATE_SCHEMA ||
74
+ existingGlobalState.version !== NDJSON_GLOBAL_STATE_VERSION;
75
+ if (needsFlusherMigration) {
76
+ for (const state of existingGlobalState.writers.values()) {
77
+ const staleExitFlusher = state.exitFlusher;
78
+ const currentExitFlusher = () => flushStateSync(state);
79
+ state.exitFlusher = currentExitFlusher;
80
+ existingGlobalState.exitFlushers.delete(staleExitFlusher);
81
+ existingGlobalState.exitFlushers.add(currentExitFlusher);
82
+ }
83
+ }
84
+ existingGlobalState.schema = NDJSON_GLOBAL_STATE_SCHEMA;
85
+ existingGlobalState.version = NDJSON_GLOBAL_STATE_VERSION;
86
+ ndjsonGlobalState = existingGlobalState;
87
+ }
88
+ else if (existingGlobalState === undefined) {
89
+ ndjsonGlobalState = (globalStateHost[NDJSON_GLOBAL_STATE_KEY] = {
90
+ schema: NDJSON_GLOBAL_STATE_SCHEMA,
91
+ version: NDJSON_GLOBAL_STATE_VERSION,
92
+ writers: new Map(),
93
+ exitFlushers: new Set(),
94
+ exitHandlerRegistered: false,
95
+ registeredLogFiles: new Set(),
96
+ });
97
+ }
98
+ else if (isLegacyGlobalState(existingGlobalState)) {
99
+ // Do not mutate or replace this state: its private queues and exit flusher
100
+ // closures are not observable, so adopting it would falsely claim safety.
101
+ // New facades fail closed in requireCurrentGlobalState().
102
+ legacyGlobalState = existingGlobalState;
103
+ }
104
+ const exitFlushers = ndjsonGlobalState?.exitFlushers ??
105
+ legacyGlobalState?.exitFlushers ??
106
+ new Set();
107
+ const registeredLogFiles = ndjsonGlobalState?.registeredLogFiles ??
108
+ legacyGlobalState?.registeredLogFiles ??
109
+ new Set();
110
+ /** Test-only view of the canonical per-file exit flushers (see ndjson-logger.test.ts). */
25
111
  export function _exitFlushersForTest() {
26
112
  return exitFlushers;
27
113
  }
@@ -37,7 +123,6 @@ export function _exitFlushersForTest() {
37
123
  // `logs/{date}.jsonl`) is deliberately NOT registered: those already live
38
124
  // under the `logs/` subdirectory and are covered by log-cleanup's separate
39
125
  // `*.jsonl` daily-log sweep, not the single-file rotation list.
40
- const registeredLogFiles = new Set();
41
126
  /** Every absolute path registered by a static-filePath createNdjsonLogger instance. */
42
127
  export function getRegisteredLogFiles() {
43
128
  return registeredLogFiles;
@@ -46,129 +131,275 @@ export function getRegisteredLogFiles() {
46
131
  export function _resetRegisteredLogFilesForTest() {
47
132
  registeredLogFiles.clear();
48
133
  }
49
- function registerExitFlusher(flushSync) {
50
- exitFlushers.add(flushSync);
51
- if (!exitHandlerRegistered) {
52
- exitHandlerRegistered = true;
134
+ function requireCurrentGlobalState() {
135
+ if (ndjsonGlobalState)
136
+ return ndjsonGlobalState;
137
+ throw new Error("createNdjsonLogger: incompatible module graph state; a pre-7e4b9120 " +
138
+ "logger graph may still own private queues. Restart the process before " +
139
+ "loading the current logger graph");
140
+ }
141
+ function registerWriter(state) {
142
+ const globalState = requireCurrentGlobalState();
143
+ if (!exitFlushers.has(state.exitFlusher))
144
+ exitFlushers.add(state.exitFlusher);
145
+ if (!globalState.exitHandlerRegistered) {
146
+ globalState.exitHandlerRegistered = true;
53
147
  process.on("exit", () => {
54
- for (const flush of exitFlushers) {
55
- try {
56
- flush();
57
- }
58
- catch { }
59
- }
148
+ for (const flush of exitFlushers)
149
+ runBestEffort(flush);
60
150
  });
61
151
  }
62
152
  }
63
- export function createNdjsonLogger(options) {
64
- if (typeof options.filePath === "string") {
65
- registeredLogFiles.add(options.filePath);
66
- }
67
- const queue = [];
68
- let drainPromise = null;
69
- let ensuredDir = false;
70
- function ensureDir(file) {
71
- if (ensuredDir)
72
- return;
73
- try {
74
- fs.mkdirSync(path.dirname(file), { recursive: true });
75
- ensuredDir = true;
153
+ function normalizeLogPath(file) {
154
+ return normalizeFilePath(path.resolve(file));
155
+ }
156
+ function assertCompatibleWriterOptions(existing, maxBytes, backupPath) {
157
+ if (existing.maxBytes === maxBytes && existing.backupPath === backupPath)
158
+ return;
159
+ throw new Error(`createNdjsonLogger: incompatible options for shared path ${existing.file}; ` +
160
+ `the first writer's maxBytes/backupPath must be reused`);
161
+ }
162
+ function applyQueueItemSync(state, item) {
163
+ ensureDirSync(state);
164
+ runBestEffort(() => {
165
+ if (item.kind === "truncate") {
166
+ fs.writeFileSync(state.file, "");
167
+ }
168
+ else {
169
+ rotateIfNeeded(state);
170
+ fs.appendFileSync(state.file, item.line);
76
171
  }
77
- catch { }
172
+ });
173
+ }
174
+ function flushStateSync(state) {
175
+ // Drain the in-memory queue synchronously — safe at process exit.
176
+ // The in-flight async batch is INCLUDED even though its appendFile may
177
+ // also land: if the process dies before the threadpool issues that
178
+ // write, skipping the prefix would drop the whole batch. The per-line
179
+ // writer deliberately traded duplicate lines at exit for never-drops
180
+ // (#935 review) — keep that trade. If a late in-flight append crosses a
181
+ // truncate, the post-truncate operations are replayed after that append
182
+ // completes so pre-truncate data cannot be reintroduced.
183
+ const inFlight = state.inFlightBatch;
184
+ const inFlightAtHead = inFlight !== null &&
185
+ inFlight.every((item, index) => state.queue[index] === item);
186
+ const repairStart = inFlightAtHead
187
+ ? inFlight?.[0]?.kind === "truncate"
188
+ ? 0
189
+ : state.queue.findIndex((item) => item.kind === "truncate")
190
+ : -1;
191
+ if (state.syncRepairItems) {
192
+ state.syncRepairItems.push(...state.queue);
193
+ }
194
+ else if (repairStart >= 0) {
195
+ state.syncRepairItems = state.queue.slice(repairStart);
196
+ }
197
+ while (state.queue.length > 0) {
198
+ const item = state.queue.shift();
199
+ applyQueueItemSync(state, item);
78
200
  }
79
- function rotateIfNeeded(file) {
80
- if (options.maxBytes === undefined)
201
+ }
202
+ function createWriterState(file, maxBytes, backupPath) {
203
+ const globalState = requireCurrentGlobalState();
204
+ const existing = globalState.writers.get(file);
205
+ if (existing) {
206
+ // A partially initialized global state from another graph still needs to
207
+ // be enrolled, but it must never get a second queue or exit flusher.
208
+ assertCompatibleWriterOptions(existing, maxBytes, backupPath);
209
+ if (!exitFlushers.has(existing.exitFlusher))
210
+ registerWriter(existing);
211
+ return existing;
212
+ }
213
+ const state = {};
214
+ state.file = file;
215
+ state.maxBytes = maxBytes;
216
+ state.backupPath = backupPath;
217
+ state.queue = [];
218
+ state.drainPromise = null;
219
+ state.inFlightBatch = null;
220
+ state.syncRepairItems = null;
221
+ state.ensuredDir = false;
222
+ state.exitFlusher = () => flushStateSync(state);
223
+ globalState.writers.set(file, state);
224
+ registerWriter(state);
225
+ return state;
226
+ }
227
+ function ensureDirSync(state) {
228
+ if (state.ensuredDir)
229
+ return;
230
+ runBestEffort(() => {
231
+ fs.mkdirSync(path.dirname(state.file), { recursive: true });
232
+ state.ensuredDir = true;
233
+ });
234
+ }
235
+ async function ensureDirAsync(state) {
236
+ if (state.ensuredDir)
237
+ return;
238
+ try {
239
+ await fs.promises.mkdir(path.dirname(state.file), { recursive: true });
240
+ state.ensuredDir = true;
241
+ }
242
+ catch {
243
+ // telemetry is best-effort
244
+ }
245
+ }
246
+ function rotateIfNeeded(state) {
247
+ if (state.maxBytes === undefined)
248
+ return;
249
+ try {
250
+ const size = fs.statSync(state.file).size;
251
+ if (size < state.maxBytes)
81
252
  return;
253
+ const backup = state.backupPath ?? `${state.file}.1`;
254
+ runBestEffort(() => fs.rmSync(backup, { force: true }));
255
+ fs.renameSync(state.file, backup);
256
+ }
257
+ catch {
258
+ // no file yet, or rename raced — nothing to rotate
259
+ }
260
+ }
261
+ async function applyQueueItemAsync(state, item) {
262
+ await ensureDirAsync(state);
263
+ try {
264
+ if (item.kind === "truncate") {
265
+ await fs.promises.writeFile(state.file, "");
266
+ }
267
+ else {
268
+ // Rotation is deliberately synchronous here. This function is only
269
+ // reached from the already-deferred drain, and keeping stat/rm/rename
270
+ // in one synchronous section prevents flushSync from racing a late
271
+ // async rename after it has written new data.
272
+ rotateIfNeeded(state);
273
+ await fs.promises.appendFile(state.file, item.line);
274
+ }
275
+ }
276
+ catch {
277
+ // telemetry is best-effort
278
+ }
279
+ }
280
+ async function drainLoop(state) {
281
+ // Peek, write, then remove — an item stays in the queue until it is on
282
+ // disk, so a teardown flushSync (which abandons this async loop) never
283
+ // drops an item this loop had already dequeued but not yet written.
284
+ while (state.queue.length > 0) {
285
+ const item = state.queue[0];
286
+ await ensureDirAsync(state);
287
+ const truncateIndex = item.kind === "line"
288
+ ? state.queue.findIndex((queued) => queued.kind === "truncate")
289
+ : 0;
290
+ const pendingEnd = truncateIndex === -1 ? state.queue.length : truncateIndex;
291
+ const pending = item.kind === "truncate"
292
+ ? [item]
293
+ : state.queue.slice(0, pendingEnd);
294
+ state.inFlightBatch = pending;
82
295
  try {
83
- const size = fs.statSync(file).size;
84
- if (size < options.maxBytes)
85
- return;
86
- const backup = options.backupPath
87
- ? resolve(options.backupPath)
88
- : `${file}.1`;
89
- try {
90
- fs.rmSync(backup, { force: true });
296
+ if (item.kind === "truncate") {
297
+ await fs.promises.writeFile(state.file, "");
298
+ }
299
+ else {
300
+ // Rotation is kept synchronous inside the deferred drain. The
301
+ // append remains async, but no awaited rotation step can run after
302
+ // flushSync has written to the active file.
303
+ rotateIfNeeded(state);
304
+ await fs.promises.appendFile(state.file, pending
305
+ .map((queued) => queued.line)
306
+ .join(""));
91
307
  }
92
- catch { }
93
- fs.renameSync(file, backup);
94
308
  }
95
309
  catch {
96
- // no file yet, or rename raced — nothing to rotate
310
+ // telemetry is best-effort
97
311
  }
98
- }
99
- async function drainLoop() {
100
- // Peek, write, then remove — an item stays in the queue until it is on
101
- // disk, so a teardown flushSync (which abandons this async loop) never
102
- // drops an item this loop had already dequeued but not yet written.
103
- while (queue.length > 0) {
104
- const item = queue[0];
105
- const file = resolve(options.filePath);
106
- ensureDir(file);
107
- try {
108
- if (item.kind === "truncate") {
109
- await fs.promises.writeFile(file, "");
110
- }
111
- else {
112
- rotateIfNeeded(file);
113
- await fs.promises.appendFile(file, item.line);
312
+ for (const written of pending) {
313
+ // flushSync may have drained this prefix while the append is in
314
+ // flight. Never remove newer items from a later enqueue.
315
+ if (state.queue[0] !== written)
316
+ break;
317
+ state.queue.shift();
318
+ }
319
+ if (state.inFlightBatch === pending) {
320
+ state.inFlightBatch = null;
321
+ const repairItems = state.syncRepairItems;
322
+ state.syncRepairItems = null;
323
+ if (repairItems) {
324
+ for (const repairItem of repairItems) {
325
+ await applyQueueItemAsync(state, repairItem);
114
326
  }
115
327
  }
116
- catch {
117
- // telemetry is best-effort
118
- }
119
- queue.shift();
120
328
  }
121
329
  }
122
- function drain() {
123
- // Serialize: a single in-flight drain owns the queue. flush() awaits this
124
- // same promise, so it never resolves before pending writes land. The loop
125
- // re-checks queue.length, so items enqueued mid-drain are picked up before
126
- // the promise settles — no stranded item, no second concurrent drainer.
127
- if (!drainPromise) {
128
- drainPromise = drainLoop().finally(() => {
129
- drainPromise = null;
130
- });
131
- }
132
- return drainPromise;
330
+ }
331
+ function drain(state) {
332
+ // Serialize: a single in-flight drain owns the canonical per-file queue.
333
+ // This guard lives in global state, so module re-evaluation cannot create a
334
+ // second drainer for the same path.
335
+ if (!state.drainPromise) {
336
+ state.drainPromise = Promise.resolve()
337
+ .then(() => drainLoop(state))
338
+ .finally(() => {
339
+ state.drainPromise = null;
340
+ });
341
+ }
342
+ return state.drainPromise;
343
+ }
344
+ export function createNdjsonLogger(options) {
345
+ const states = new Set();
346
+ // Static logger paths are canonicalized once. Lazy diagnostic paths are
347
+ // resolved at enqueue time, but each resolved raw path is canonicalized only
348
+ // once (normally once per date), keeping realpath work off the hot path.
349
+ const canonicalPaths = new Map();
350
+ const staticFile = typeof options.filePath === "string"
351
+ ? normalizeLogPath(options.filePath)
352
+ : undefined;
353
+ const staticBackupPath = typeof options.backupPath === "string" && options.maxBytes !== undefined
354
+ ? normalizeLogPath(options.backupPath)
355
+ : undefined;
356
+ function canonicalPath(file) {
357
+ const raw = path.resolve(file);
358
+ const cached = canonicalPaths.get(raw);
359
+ if (cached)
360
+ return cached;
361
+ const normalized = normalizeLogPath(raw);
362
+ canonicalPaths.set(raw, normalized);
363
+ return normalized;
364
+ }
365
+ function stateForCall() {
366
+ const file = staticFile ?? canonicalPath(resolve(options.filePath));
367
+ const backupPath = options.maxBytes !== undefined && options.backupPath
368
+ ? (staticBackupPath ?? canonicalPath(resolve(options.backupPath)))
369
+ : undefined;
370
+ const state = createWriterState(file, options.maxBytes, backupPath);
371
+ states.add(state);
372
+ return state;
373
+ }
374
+ if (staticFile !== undefined) {
375
+ const state = stateForCall();
376
+ registeredLogFiles.add(state.file);
133
377
  }
134
378
  function enqueue(item) {
135
- queue.push(item);
136
- void drain();
137
- }
138
- function flushSync() {
139
- // Drain the in-memory queue synchronously — safe at process exit.
140
- while (queue.length > 0) {
141
- const item = queue.shift();
142
- const file = resolve(options.filePath);
143
- ensureDir(file);
144
- try {
145
- if (item.kind === "truncate") {
146
- fs.writeFileSync(file, "");
147
- }
148
- else {
149
- rotateIfNeeded(file);
150
- fs.appendFileSync(file, item.line);
151
- }
152
- }
153
- catch { }
154
- }
379
+ const state = stateForCall();
380
+ state.queue.push(item);
381
+ void drain(state);
155
382
  }
156
- // Best-effort teardown flush of anything still buffered, via the single
157
- // shared exit handler. appendFileSync is fine here — not the hot path.
158
- registerExitFlusher(flushSync);
159
383
  return {
160
384
  log(obj) {
161
- enqueue({ kind: "line", line: `${JSON.stringify(obj)}\n` });
385
+ const serialized = String(JSON.stringify(obj));
386
+ enqueue({
387
+ kind: "line",
388
+ line: `${redactSecrets(serialized)}\n`,
389
+ });
162
390
  },
163
391
  append(line) {
164
- enqueue({ kind: "line", line: `${line}\n` });
392
+ enqueue({ kind: "line", line: `${redactSecrets(line)}\n` });
165
393
  },
166
394
  truncate() {
167
395
  enqueue({ kind: "truncate" });
168
396
  },
169
397
  async flush() {
170
- await drain();
398
+ await Promise.all([...states].map((state) => drain(state)));
399
+ },
400
+ flushSync() {
401
+ for (const state of states)
402
+ flushStateSync(state);
171
403
  },
172
- flushSync,
173
404
  };
174
405
  }
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Why: opengrep has no `workspace/diagnostic` pull support (push-only, per
12
12
  * `docs/servercapabilities.md`), and `reopenOnResync: true`
13
- * (`clients/lsp/server-strategies.ts`) means every LSP touch already forces a
13
+ * (`clients/lsp/wait-policy/strategies.ts`) means every LSP touch already forces a
14
14
  * full re-scan of that one file — there's no incremental efficiency lost by
15
15
  * moving bulk scans off the per-file touch loop. On a full sweep the old path
16
16
  * instead paid opengrep's full per-file wait-tier budget serially, one file at
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import { detectLineEnding, normalizeToLF, restoreLineEndings, } from "./host-edit-normalize.js";
3
+ import { boundedEditIndexes, createReadGuardEditBatchSummary, formatBoundedEditIndexes, logReadGuardEvent, } from "./read-guard-logger.js";
3
4
  function replaceOnce(content, oldText, newText) {
4
5
  const idx = content.indexOf(oldText);
5
6
  if (idx === -1)
@@ -16,6 +17,7 @@ function replaceOnce(content, oldText, newText) {
16
17
  * than logged as applied.
17
18
  */
18
19
  export async function applyPartiallyApplicableEdits(args) {
20
+ const startedAt = Date.now();
19
21
  const raw = fs.readFileSync(args.filePath, "utf-8");
20
22
  // Detect + restore line endings the way the host edit tool does:
21
23
  // first-occurrence-wins detection (not "any CRLF present") and lone-CR -> LF
@@ -24,22 +26,135 @@ export async function applyPartiallyApplicableEdits(args) {
24
26
  const ending = detectLineEnding(raw);
25
27
  let content = normalizeToLF(raw);
26
28
  const applied = [];
29
+ const skipped = [];
27
30
  for (const edit of args.edits) {
28
31
  const oldText = normalizeToLF(edit.oldText);
29
32
  const newText = normalizeToLF(edit.newText ?? "");
30
33
  const replaced = replaceOnce(content, oldText, newText);
31
- if (!replaced.changed)
34
+ if (!replaced.changed) {
35
+ skipped.push(edit.originalIndex);
32
36
  continue;
37
+ }
33
38
  content = replaced.content;
34
39
  applied.push(edit.originalIndex);
35
40
  }
36
- if (applied.length > 0) {
37
- fs.writeFileSync(args.filePath, restoreLineEndings(content, ending), "utf-8");
41
+ let commitStatus = applied.length > 0 ? "committed" : "no_changes";
42
+ try {
43
+ if (applied.length > 0) {
44
+ fs.writeFileSync(args.filePath, restoreLineEndings(content, ending), "utf-8");
45
+ }
38
46
  }
39
- const postEditOutput = applied.length > 0 ? await args.afterWrite?.() : undefined;
47
+ catch (error) {
48
+ commitStatus = "failed";
49
+ if (args.summary || args.correlationId) {
50
+ const summary = createReadGuardEditBatchSummary({
51
+ ...(args.summary ?? {
52
+ requestedIndexes: boundedEditIndexes(args.edits.slice(0, 100).map((edit) => edit.originalIndex)),
53
+ requestedTotal: args.edits.length,
54
+ }),
55
+ appliedIndexes: [],
56
+ appliedTotal: 0,
57
+ participantIds: args.correlationId ? [args.correlationId] : undefined,
58
+ participantTotal: args.correlationId ? 1 : undefined,
59
+ commitStatus,
60
+ terminalStatus: "failed",
61
+ durationMs: Date.now() - startedAt,
62
+ });
63
+ logReadGuardEvent({
64
+ event: "edit_batch_summary",
65
+ correlationId: args.correlationId,
66
+ filePath: args.filePath,
67
+ metadata: { tool: "edit", editBatchSummary: summary },
68
+ });
69
+ }
70
+ throw error;
71
+ }
72
+ let postEditOutput;
73
+ let postEditStatus = "not_run";
74
+ if (applied.length > 0 && args.afterWrite) {
75
+ try {
76
+ postEditOutput = await args.afterWrite();
77
+ postEditStatus = "succeeded";
78
+ }
79
+ catch (error) {
80
+ // The write is committed. Preserve the existing caller behavior for
81
+ // uninstrumented callers; the read-guard path records and handles it.
82
+ postEditStatus = "failed";
83
+ if (!args.summary && !args.correlationId)
84
+ throw error;
85
+ }
86
+ }
87
+ if (!args.summary && !args.correlationId) {
88
+ return {
89
+ appliedCount: applied.length,
90
+ appliedIndices: formatBoundedEditIndexes(applied),
91
+ postEditOutput,
92
+ };
93
+ }
94
+ const baseSummary = args.summary ??
95
+ createReadGuardEditBatchSummary({
96
+ requestedIndexes: boundedEditIndexes(args.edits.slice(0, 100).map((edit) => edit.originalIndex)),
97
+ requestedTotal: args.edits.length,
98
+ resolvedIndexes: boundedEditIndexes(args.edits.slice(0, 100).map((edit) => edit.originalIndex)),
99
+ resolvedTotal: args.edits.length,
100
+ });
101
+ const summary = createReadGuardEditBatchSummary({
102
+ ...baseSummary,
103
+ rejectedReasons: [
104
+ ...baseSummary.rejectedReasons,
105
+ ...skipped.slice(0, 100).map((index) => ({
106
+ index,
107
+ code: "replace_once_skipped",
108
+ })),
109
+ ],
110
+ rejectedTotal: baseSummary.rejectedTotal + skipped.length,
111
+ appliedIndexes: applied,
112
+ appliedTotal: applied.length,
113
+ participantIds: args.correlationId
114
+ ? [...baseSummary.participantIds, args.correlationId]
115
+ : baseSummary.participantIds,
116
+ participantTotal: args.correlationId
117
+ ? baseSummary.participantTotal + 1
118
+ : baseSummary.participantTotal,
119
+ commitStatus,
120
+ postEditStatus,
121
+ terminalStatus: postEditStatus === "failed"
122
+ ? "failed"
123
+ : applied.length === 0
124
+ ? "skipped"
125
+ : "success",
126
+ durationMs: Date.now() - startedAt,
127
+ });
128
+ if (postEditStatus === "failed") {
129
+ logReadGuardEvent({
130
+ event: "edit_post_edit_pipeline_failed",
131
+ correlationId: args.correlationId,
132
+ filePath: args.filePath,
133
+ metadata: {
134
+ tool: "edit",
135
+ commitStatus,
136
+ appliedCount: applied.length,
137
+ appliedIndexes: applied.slice(0, 100),
138
+ appliedTotal: applied.length,
139
+ },
140
+ });
141
+ }
142
+ logReadGuardEvent({
143
+ event: "edit_batch_summary",
144
+ correlationId: args.correlationId,
145
+ filePath: args.filePath,
146
+ metadata: { tool: "edit", editBatchSummary: summary },
147
+ });
40
148
  return {
41
149
  appliedCount: applied.length,
42
- appliedIndices: applied.map((index) => `edits[${index}]`).join(", "),
150
+ appliedTotal: applied.length,
151
+ appliedIndices: formatBoundedEditIndexes(applied),
43
152
  postEditOutput,
153
+ skippedCount: skipped.length,
154
+ skippedTotal: skipped.length,
155
+ skippedIndices: formatBoundedEditIndexes(skipped),
156
+ indexesTruncated: applied.length > 100 || skipped.length > 100 || summary.indexesTruncated,
157
+ postEditStatus,
158
+ summary,
44
159
  };
45
160
  }