pi-lens 4.2.1 → 4.4.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 (671) hide show
  1. package/CHANGELOG.md +1562 -0
  2. package/README.md +157 -24
  3. package/config/dependency-cruiser-eager-allowlist.json +6 -2
  4. package/dist/chunk-22AVIGXS.js +560 -0
  5. package/dist/chunk-27BBHSZC.js +106 -0
  6. package/dist/chunk-2BEMIWEW.js +51 -0
  7. package/dist/chunk-2EECBNC5.js +89 -0
  8. package/dist/chunk-34U7PLGE.js +23 -0
  9. package/dist/chunk-3I5QFT3N.js +181 -0
  10. package/dist/chunk-3MED75HT.js +37 -0
  11. package/dist/chunk-47D2BO4S.js +87 -0
  12. package/dist/chunk-4RERDEXR.js +348 -0
  13. package/dist/chunk-4W2PLC4J.js +27 -0
  14. package/dist/chunk-52H5B5TC.js +202 -0
  15. package/dist/chunk-54B42YRX.js +27 -0
  16. package/dist/chunk-5RXBNSW2.js +266 -0
  17. package/dist/chunk-5THXD6X5.js +201 -0
  18. package/dist/chunk-5W3R6LFT.js +24 -0
  19. package/dist/chunk-5ZEJHV35.js +341 -0
  20. package/dist/chunk-6P62QVEA.js +118 -0
  21. package/dist/chunk-6QIUX4EP.js +459 -0
  22. package/dist/chunk-6VCD4VKZ.js +78 -0
  23. package/dist/chunk-7F72PXY2.js +326 -0
  24. package/dist/chunk-7FBF7NPR.js +998 -0
  25. package/dist/chunk-7U2IDNXN.js +37 -0
  26. package/dist/chunk-ABBOA7UD.js +157 -0
  27. package/dist/chunk-AHDW3GPE.js +50 -0
  28. package/dist/chunk-AJ3QXTRQ.js +756 -0
  29. package/dist/chunk-AJVLWNFN.js +1610 -0
  30. package/dist/chunk-AJXODZY6.js +112 -0
  31. package/dist/chunk-ARFCXG7S.js +84 -0
  32. package/dist/chunk-AS44CFQ4.js +708 -0
  33. package/dist/chunk-AWE5TMPO.js +38 -0
  34. package/dist/chunk-B3I2UUT4.js +4692 -0
  35. package/dist/chunk-BRJKOBZP.js +331 -0
  36. package/dist/chunk-BW5KFIQC.js +59 -0
  37. package/dist/chunk-C2AUYXQH.js +149 -0
  38. package/dist/chunk-CGGAIP7J.js +29 -0
  39. package/dist/chunk-D6QS3U6M.js +30 -0
  40. package/dist/chunk-DFVMIKMQ.js +349 -0
  41. package/dist/chunk-DIULIWKU.js +169 -0
  42. package/dist/chunk-DQ2NZ7C4.js +2317 -0
  43. package/dist/chunk-E2WFBRHM.js +213 -0
  44. package/dist/chunk-E4MAEDPM.js +50 -0
  45. package/dist/chunk-EOKRQ3SA.js +30 -0
  46. package/dist/chunk-F3AU2TMM.js +2626 -0
  47. package/dist/chunk-F3P7VQ7H.js +170 -0
  48. package/dist/chunk-FC4RP4I2.js +12 -0
  49. package/dist/chunk-FJNKANOS.js +32 -0
  50. package/dist/chunk-GUOGVTCQ.js +146 -0
  51. package/dist/chunk-GVXA6CCE.js +18 -0
  52. package/dist/chunk-HHYAELLJ.js +2334 -0
  53. package/dist/chunk-HMFXILPV.js +83 -0
  54. package/dist/chunk-HVUZXSWK.js +421 -0
  55. package/dist/chunk-HWBZIGMJ.js +62 -0
  56. package/dist/chunk-I3R7GKDR.js +17 -0
  57. package/dist/chunk-I7SPGOWI.js +55 -0
  58. package/dist/chunk-IIXOAGHR.js +38 -0
  59. package/dist/chunk-IVP5KAB7.js +2250 -0
  60. package/dist/chunk-IXEBJSUE.js +22 -0
  61. package/dist/chunk-JOLITGCC.js +27 -0
  62. package/dist/chunk-K6VYLB2U.js +145 -0
  63. package/dist/chunk-LJDODBBZ.js +78 -0
  64. package/dist/chunk-LNAKEO63.js +78 -0
  65. package/dist/chunk-LSMJ6KBE.js +4139 -0
  66. package/dist/chunk-LW6XKDZQ.js +5565 -0
  67. package/dist/chunk-N3YQJI6O.js +899 -0
  68. package/dist/chunk-NABLC7W3.js +148 -0
  69. package/dist/chunk-NKOUFPQF.js +659 -0
  70. package/dist/chunk-NL5GLMFI.js +211 -0
  71. package/dist/chunk-NMABWBZN.js +327 -0
  72. package/dist/chunk-NTHH2J74.js +27 -0
  73. package/dist/chunk-NTUJEGDZ.js +59 -0
  74. package/dist/chunk-NXL4FFQQ.js +47 -0
  75. package/dist/chunk-NXPS2L6U.js +466 -0
  76. package/dist/chunk-O5WILGMI.js +42 -0
  77. package/dist/chunk-O6TQT6RI.js +48 -0
  78. package/dist/chunk-OAXNDZJ7.js +323 -0
  79. package/dist/chunk-ODEEOXPH.js +44 -0
  80. package/dist/chunk-OQGFCZJ4.js +4533 -0
  81. package/dist/chunk-ORTMY4Q4.js +282 -0
  82. package/dist/chunk-OS32RNR2.js +442 -0
  83. package/dist/chunk-PAK6IDME.js +189 -0
  84. package/dist/chunk-PWFHBTSF.js +406 -0
  85. package/dist/chunk-Q5U6FMDI.js +2352 -0
  86. package/dist/chunk-QOZXJ4NA.js +3364 -0
  87. package/dist/chunk-QYF5S6P6.js +45 -0
  88. package/dist/chunk-RIISMHKN.js +36 -0
  89. package/dist/chunk-RPD4ZSB7.js +25 -0
  90. package/dist/chunk-RPD5K5NC.js +79 -0
  91. package/dist/chunk-RUOG5D2C.js +262 -0
  92. package/dist/chunk-T4ERT73Y.js +599 -0
  93. package/dist/chunk-T6D4Q4AQ.js +54 -0
  94. package/dist/chunk-TGPXLY2J.js +391 -0
  95. package/dist/chunk-TLGFBM3F.js +12116 -0
  96. package/dist/chunk-TO6WL3GM.js +88 -0
  97. package/dist/chunk-TT22N74J.js +517 -0
  98. package/dist/chunk-TVN5N6GY.js +21216 -0
  99. package/dist/chunk-U3VGR4VU.js +308 -0
  100. package/dist/chunk-UK3CMEAL.js +1250 -0
  101. package/dist/chunk-UOPVJQ4N.js +103 -0
  102. package/dist/chunk-UXVNL3D7.js +23 -0
  103. package/dist/chunk-UY752QSN.js +45 -0
  104. package/dist/chunk-V5PXFSZK.js +188 -0
  105. package/dist/chunk-V7R425XG.js +398 -0
  106. package/dist/chunk-VCSRSYLF.js +81 -0
  107. package/dist/chunk-VD4JQCQC.js +162 -0
  108. package/dist/chunk-VDBPVIKD.js +62 -0
  109. package/dist/chunk-VHQLPIRJ.js +114 -0
  110. package/dist/chunk-VN2AXLEX.js +5414 -0
  111. package/dist/chunk-VNMT7C64.js +478 -0
  112. package/dist/chunk-WONERJTP.js +309 -0
  113. package/dist/chunk-XECLWQZN.js +85 -0
  114. package/dist/chunk-XIMYZ3JA.js +27 -0
  115. package/dist/chunk-XK4CIILI.js +144 -0
  116. package/dist/chunk-Y4T27COV.js +562 -0
  117. package/dist/chunk-YGVYKNSN.js +1860 -0
  118. package/dist/chunk-YLF3PPT5.js +308 -0
  119. package/dist/chunk-YSNFMJKN.js +14 -0
  120. package/dist/chunk-Z3CCVDUB.js +76 -0
  121. package/dist/chunk-Z54ZSOBZ.js +61 -0
  122. package/dist/chunk-ZAWW3AF6.js +97 -0
  123. package/dist/chunk-ZMOL6RQC.js +167 -0
  124. package/dist/index.js +19039 -8956
  125. package/dist/mcp/analyze-cli.js +186 -228
  126. package/dist/mcp/cli.js +137 -98
  127. package/dist/mcp/server.js +22592 -1564
  128. package/dist/mcp/worker.js +97 -38
  129. package/dist/probes/installer.js +135 -0
  130. package/dist/workers/project-snapshot-persist-worker.js +22 -0
  131. package/dist/workers/review-graph-persist-worker.js +15 -0
  132. package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  133. package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  134. package/docs/adr/0003-git-guard-latch-writer.md +203 -0
  135. package/docs/adr/0004-disposition-policy-seam.md +38 -0
  136. package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  137. package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  138. package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  139. package/docs/adr/0008-turn-end-lane-interface.md +165 -0
  140. package/docs/adr/0009-reported-path-attribution.md +114 -0
  141. package/docs/adr/README.md +26 -0
  142. package/docs/agent-guide.md +13 -4
  143. package/docs/agent-tools.md +60 -1
  144. package/docs/ast-grep_rules_catalog.md +6 -4
  145. package/docs/configuration.md +75 -2
  146. package/docs/engineering-principles.md +202 -0
  147. package/docs/environment-variables.md +113 -6
  148. package/docs/features.md +10 -6
  149. package/docs/globalconfig.md +7 -1
  150. package/docs/io-bridge-v2-migration.md +94 -0
  151. package/docs/language-coverage.md +17 -0
  152. package/docs/lsp-capability-matrix.md +131 -32
  153. package/docs/lsp-idle-eviction.md +124 -0
  154. package/docs/mcp.md +21 -5
  155. package/docs/merge-queue-rollout.md +77 -0
  156. package/docs/pi-lens-fixer.md +161 -39
  157. package/docs/pi-lens-investigator.md +77 -20
  158. package/docs/pi-lens-merge-policy.md +694 -0
  159. package/docs/pi-lens-monitor.md +73 -7
  160. package/docs/pi-lens-retro.md +6 -4
  161. package/docs/pi-lens-reviewer.md +190 -34
  162. package/docs/pi-lens-subagent.md +247 -37
  163. package/docs/pi-lens-warden.md +18 -7
  164. package/docs/public-api-stability.md +56 -0
  165. package/docs/release-qa-baseline.md +86 -17
  166. package/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  167. package/docs/servercapabilities.md +66 -52
  168. package/docs/settings.md +13 -3
  169. package/docs/subagent-compat.md +1 -1
  170. package/docs/usage.md +5 -1
  171. package/grammars/tree-sitter-bash.wasm +0 -0
  172. package/grammars/tree-sitter-bash.wasm.json +3 -3
  173. package/package.json +23 -9
  174. package/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  175. package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  176. package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  177. package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  178. package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  179. package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  180. package/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  181. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  182. package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  183. package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  184. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  185. package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  186. package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  187. package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  188. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  189. package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  190. package/scripts/analyze-pi-lens-logs.mjs +1275 -62
  191. package/scripts/download-grammars.js +5 -0
  192. package/scripts/grammars.lock.json +6 -1
  193. package/scripts/install-selftest.mjs +38 -35
  194. package/scripts/lib/host-provided-deps.mjs +13 -4
  195. package/scripts/lib/web-tree-sitter-dir.mjs +141 -0
  196. package/dist/clients/actionable-warnings-logger.js +0 -35
  197. package/dist/clients/actionable-warnings.js +0 -1594
  198. package/dist/clients/advisory-provenance.js +0 -457
  199. package/dist/clients/agent-behavior-client.js +0 -147
  200. package/dist/clients/agent-nudge.js +0 -325
  201. package/dist/clients/analysed-root.js +0 -1
  202. package/dist/clients/ast-grep-client.js +0 -731
  203. package/dist/clients/ast-grep-rule-manager.js +0 -146
  204. package/dist/clients/ast-grep-tool-logger.js +0 -118
  205. package/dist/clients/ast-grep-types.js +0 -10
  206. package/dist/clients/ast-grep-yaml-synth.js +0 -154
  207. package/dist/clients/atomic-write-staging.js +0 -75
  208. package/dist/clients/atomic-write.js +0 -189
  209. package/dist/clients/availability-probe-flight.js +0 -12
  210. package/dist/clients/bash-file-access.js +0 -1056
  211. package/dist/clients/biome-client.js +0 -357
  212. package/dist/clients/blocker-freshness.js +0 -837
  213. package/dist/clients/blocker-past-eof.js +0 -129
  214. package/dist/clients/bootstrap.js +0 -569
  215. package/dist/clients/bounded-cache.js +0 -190
  216. package/dist/clients/bounded-pid-file-lock.js +0 -202
  217. package/dist/clients/bounded-telemetry.js +0 -295
  218. package/dist/clients/build-identity.js +0 -95
  219. package/dist/clients/bundled-resource-health.js +0 -113
  220. package/dist/clients/bus-events-logger.js +0 -105
  221. package/dist/clients/bus-publish.js +0 -171
  222. package/dist/clients/cache/rule-cache.js +0 -190
  223. package/dist/clients/cache-manager.js +0 -465
  224. package/dist/clients/cache-observability.js +0 -1313
  225. package/dist/clients/call-graph.js +0 -852
  226. package/dist/clients/cargo-manifest.js +0 -422
  227. package/dist/clients/cascade-budget.js +0 -196
  228. package/dist/clients/cascade-format.js +0 -315
  229. package/dist/clients/cascade-logger.js +0 -31
  230. package/dist/clients/cascade-types.js +0 -9
  231. package/dist/clients/child-unref.js +0 -115
  232. package/dist/clients/code-quality-warnings.js +0 -286
  233. package/dist/clients/codebase-model.js +0 -173
  234. package/dist/clients/collateral-test-role.js +0 -40
  235. package/dist/clients/complexity-client.js +0 -462
  236. package/dist/clients/config-core/deny.js +0 -221
  237. package/dist/clients/config-core/index.js +0 -50
  238. package/dist/clients/config-core/merge.js +0 -357
  239. package/dist/clients/config-core/normalize.js +0 -340
  240. package/dist/clients/config-core/process-spec.js +0 -248
  241. package/dist/clients/config-core/provenance.js +0 -164
  242. package/dist/clients/config-core/records.js +0 -218
  243. package/dist/clients/config-core/resolve.js +0 -125
  244. package/dist/clients/config-core/safe-object.js +0 -78
  245. package/dist/clients/config-core/schema.js +0 -165
  246. package/dist/clients/config-diagnostic-codes.js +0 -283
  247. package/dist/clients/config-locations.js +0 -160
  248. package/dist/clients/config-resolve.js +0 -792
  249. package/dist/clients/config-schema.js +0 -251
  250. package/dist/clients/config-warn.js +0 -407
  251. package/dist/clients/console-guard-install.js +0 -26
  252. package/dist/clients/cooperative-budget.js +0 -42
  253. package/dist/clients/dead-code-client.js +0 -510
  254. package/dist/clients/dead-code-logger.js +0 -30
  255. package/dist/clients/deadline-utils.js +0 -280
  256. package/dist/clients/debug-handles.js +0 -211
  257. package/dist/clients/debug-heap.js +0 -174
  258. package/dist/clients/deferred-lsp-work.js +0 -106
  259. package/dist/clients/degradation-ledger.js +0 -396
  260. package/dist/clients/demoted-finding-render.js +0 -169
  261. package/dist/clients/dependency-checker.js +0 -862
  262. package/dist/clients/deps/ast-grep-napi.js +0 -22
  263. package/dist/clients/deps/js-yaml.js +0 -9
  264. package/dist/clients/deps/minimatch.js +0 -4
  265. package/dist/clients/deps/pi-tui.js +0 -14
  266. package/dist/clients/deps/typebox.js +0 -14
  267. package/dist/clients/deps/vscode-jsonrpc.js +0 -1
  268. package/dist/clients/deps/web-tree-sitter.js +0 -24
  269. package/dist/clients/diagnostic-dispositions.js +0 -594
  270. package/dist/clients/diagnostic-line-freshness.js +0 -305
  271. package/dist/clients/diagnostic-logger.js +0 -73
  272. package/dist/clients/diagnostic-tracker.js +0 -105
  273. package/dist/clients/diagnostics-publish.js +0 -230
  274. package/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  275. package/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  276. package/dist/clients/dispatch/collect-later-tier.js +0 -35
  277. package/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  278. package/dist/clients/dispatch/dispatcher.js +0 -1269
  279. package/dist/clients/dispatch/fact-provider-types.js +0 -1
  280. package/dist/clients/dispatch/fact-rule-runner.js +0 -17
  281. package/dist/clients/dispatch/fact-runner.js +0 -30
  282. package/dist/clients/dispatch/fact-scheduler.js +0 -68
  283. package/dist/clients/dispatch/fact-store.js +0 -377
  284. package/dist/clients/dispatch/facts/comment-facts.js +0 -22
  285. package/dist/clients/dispatch/facts/file-content.js +0 -19
  286. package/dist/clients/dispatch/facts/function-facts.js +0 -348
  287. package/dist/clients/dispatch/facts/import-facts.js +0 -204
  288. package/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  289. package/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  290. package/dist/clients/dispatch/finding-policy.js +0 -222
  291. package/dist/clients/dispatch/indent-detect.js +0 -411
  292. package/dist/clients/dispatch/inline-suppressions.js +0 -70
  293. package/dist/clients/dispatch/integration.js +0 -2377
  294. package/dist/clients/dispatch/lazy.js +0 -11
  295. package/dist/clients/dispatch/pending-runner-findings.js +0 -87
  296. package/dist/clients/dispatch/plan.js +0 -278
  297. package/dist/clients/dispatch/priorities.js +0 -20
  298. package/dist/clients/dispatch/rule-id-normalize.js +0 -50
  299. package/dist/clients/dispatch/rule-ignores.js +0 -62
  300. package/dist/clients/dispatch/rule-policy.js +0 -135
  301. package/dist/clients/dispatch/rules/async-noise.js +0 -40
  302. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  303. package/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  304. package/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  305. package/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  306. package/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  307. package/dist/clients/dispatch/rules/high-complexity.js +0 -72
  308. package/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  309. package/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  310. package/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  311. package/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  312. package/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  313. package/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  314. package/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  315. package/dist/clients/dispatch/runner-context.js +0 -52
  316. package/dist/clients/dispatch/runners/actionlint.js +0 -113
  317. package/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  318. package/dist/clients/dispatch/runners/biome-check.js +0 -283
  319. package/dist/clients/dispatch/runners/cpp-check.js +0 -236
  320. package/dist/clients/dispatch/runners/credo.js +0 -80
  321. package/dist/clients/dispatch/runners/cue-vet.js +0 -398
  322. package/dist/clients/dispatch/runners/dart-analyze.js +0 -202
  323. package/dist/clients/dispatch/runners/detekt.js +0 -169
  324. package/dist/clients/dispatch/runners/dotnet-build.js +0 -159
  325. package/dist/clients/dispatch/runners/elixir-check.js +0 -210
  326. package/dist/clients/dispatch/runners/eslint.js +0 -154
  327. package/dist/clients/dispatch/runners/fact-rules.js +0 -29
  328. package/dist/clients/dispatch/runners/fish-indent.js +0 -69
  329. package/dist/clients/dispatch/runners/gleam-check.js +0 -87
  330. package/dist/clients/dispatch/runners/go-vet.js +0 -90
  331. package/dist/clients/dispatch/runners/golangci-lint.js +0 -120
  332. package/dist/clients/dispatch/runners/hadolint.js +0 -79
  333. package/dist/clients/dispatch/runners/helm-lint.js +0 -171
  334. package/dist/clients/dispatch/runners/helm-render.js +0 -940
  335. package/dist/clients/dispatch/runners/htmlhint.js +0 -108
  336. package/dist/clients/dispatch/runners/index.js +0 -108
  337. package/dist/clients/dispatch/runners/javac.js +0 -89
  338. package/dist/clients/dispatch/runners/ktlint.js +0 -136
  339. package/dist/clients/dispatch/runners/lsp.js +0 -378
  340. package/dist/clients/dispatch/runners/markdownlint.js +0 -160
  341. package/dist/clients/dispatch/runners/mypy.js +0 -101
  342. package/dist/clients/dispatch/runners/oxlint.js +0 -398
  343. package/dist/clients/dispatch/runners/php-lint.js +0 -62
  344. package/dist/clients/dispatch/runners/phpstan.js +0 -120
  345. package/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  346. package/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  347. package/dist/clients/dispatch/runners/pyright.js +0 -169
  348. package/dist/clients/dispatch/runners/rubocop.js +0 -90
  349. package/dist/clients/dispatch/runners/ruff.js +0 -89
  350. package/dist/clients/dispatch/runners/rust-clippy.js +0 -251
  351. package/dist/clients/dispatch/runners/shellcheck.js +0 -169
  352. package/dist/clients/dispatch/runners/shfmt.js +0 -102
  353. package/dist/clients/dispatch/runners/spellcheck.js +0 -121
  354. package/dist/clients/dispatch/runners/spotbugs.js +0 -259
  355. package/dist/clients/dispatch/runners/sqlfluff.js +0 -174
  356. package/dist/clients/dispatch/runners/stylelint.js +0 -158
  357. package/dist/clients/dispatch/runners/swiftlint.js +0 -154
  358. package/dist/clients/dispatch/runners/taplo.js +0 -168
  359. package/dist/clients/dispatch/runners/terragrunt.js +0 -168
  360. package/dist/clients/dispatch/runners/tflint.js +0 -110
  361. package/dist/clients/dispatch/runners/tree-sitter.js +0 -620
  362. package/dist/clients/dispatch/runners/trivy-config.js +0 -247
  363. package/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  364. package/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  365. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -85
  366. package/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  367. package/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  368. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  369. package/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  370. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  371. package/dist/clients/dispatch/runners/utils.js +0 -15
  372. package/dist/clients/dispatch/runners/vale.js +0 -116
  373. package/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  374. package/dist/clients/dispatch/runners/yamllint.js +0 -85
  375. package/dist/clients/dispatch/runners/zig-check.js +0 -91
  376. package/dist/clients/dispatch/suppress-writer.js +0 -69
  377. package/dist/clients/dispatch/tool-profile.js +0 -30
  378. package/dist/clients/dispatch/types.js +0 -21
  379. package/dist/clients/dispatch/utils/format-utils.js +0 -45
  380. package/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  381. package/dist/clients/disposition-logger.js +0 -32
  382. package/dist/clients/disposition-publish.js +0 -124
  383. package/dist/clients/durable-store.js +0 -97
  384. package/dist/clients/effective-config.js +0 -413
  385. package/dist/clients/env-utils.js +0 -83
  386. package/dist/clients/error-class.js +0 -23
  387. package/dist/clients/eval-timestamp.js +0 -17
  388. package/dist/clients/event-loop-hold.js +0 -274
  389. package/dist/clients/event-loop-monitor.js +0 -250
  390. package/dist/clients/extension-log.js +0 -504
  391. package/dist/clients/extension-mode.js +0 -73
  392. package/dist/clients/feature-hints.js +0 -103
  393. package/dist/clients/file-kinds.js +0 -412
  394. package/dist/clients/file-role.js +0 -134
  395. package/dist/clients/file-time.js +0 -160
  396. package/dist/clients/file-utils.js +0 -1032
  397. package/dist/clients/finding-delivery-gate.js +0 -463
  398. package/dist/clients/finding-identity.js +0 -88
  399. package/dist/clients/fix-worklog.js +0 -103
  400. package/dist/clients/format-events-publish.js +0 -343
  401. package/dist/clients/format-service.js +0 -222
  402. package/dist/clients/formatters-lazy.js +0 -9
  403. package/dist/clients/formatters.js +0 -1994
  404. package/dist/clients/freshness-cadence.js +0 -17
  405. package/dist/clients/freshness.js +0 -38
  406. package/dist/clients/generated-artifacts.js +0 -394
  407. package/dist/clients/generation-guard.js +0 -215
  408. package/dist/clients/git-guard.js +0 -923
  409. package/dist/clients/git-tracked-ignore.js +0 -243
  410. package/dist/clients/gitleaks-client.js +0 -509
  411. package/dist/clients/go-client.js +0 -104
  412. package/dist/clients/govulncheck-client.js +0 -605
  413. package/dist/clients/gradle-ktfmt-style.js +0 -252
  414. package/dist/clients/grammar-source.js +0 -465
  415. package/dist/clients/gzip-stage-write.js +0 -135
  416. package/dist/clients/hashline-anchor.js +0 -424
  417. package/dist/clients/hook-budgets.js +0 -76
  418. package/dist/clients/host-edit-normalize.js +0 -140
  419. package/dist/clients/host-ports.js +0 -39
  420. package/dist/clients/indent-retarget.js +0 -154
  421. package/dist/clients/install-diagnostics.js +0 -143
  422. package/dist/clients/installer/index.js +0 -5167
  423. package/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  424. package/dist/clients/installer/managed-tool-refresh.js +0 -783
  425. package/dist/clients/instance-reaper-state.js +0 -7
  426. package/dist/clients/instance-reaper.js +0 -1137
  427. package/dist/clients/instance-registry-lock.js +0 -145
  428. package/dist/clients/instance-registry.js +0 -773
  429. package/dist/clients/jscpd-client.js +0 -370
  430. package/dist/clients/json-cache-read.js +0 -65
  431. package/dist/clients/knip-client.js +0 -788
  432. package/dist/clients/language-policy.js +0 -252
  433. package/dist/clients/language-profile.js +0 -361
  434. package/dist/clients/language-registry.js +0 -603
  435. package/dist/clients/latency-logger.js +0 -719
  436. package/dist/clients/lazy-import.js +0 -21
  437. package/dist/clients/ledger-bounds.js +0 -54
  438. package/dist/clients/lens-config.js +0 -346
  439. package/dist/clients/lens-engine.js +0 -358
  440. package/dist/clients/lens-events.js +0 -125
  441. package/dist/clients/lens-flag-registry.js +0 -467
  442. package/dist/clients/lens-map.js +0 -1156
  443. package/dist/clients/live-bus-emitter.js +0 -74
  444. package/dist/clients/log-cleanup.js +0 -222
  445. package/dist/clients/lsp/aggregation.js +0 -178
  446. package/dist/clients/lsp/cascade-tier.js +0 -421
  447. package/dist/clients/lsp/client.js +0 -4465
  448. package/dist/clients/lsp/config.js +0 -689
  449. package/dist/clients/lsp/diagnostic-binding.js +0 -225
  450. package/dist/clients/lsp/document-drift.js +0 -467
  451. package/dist/clients/lsp/edits.js +0 -1219
  452. package/dist/clients/lsp/index.js +0 -8305
  453. package/dist/clients/lsp/inferred-project.js +0 -216
  454. package/dist/clients/lsp/jvm-runtime.js +0 -298
  455. package/dist/clients/lsp/language.js +0 -63
  456. package/dist/clients/lsp/launch.js +0 -725
  457. package/dist/clients/lsp/lombok.js +0 -158
  458. package/dist/clients/lsp/path-utils.js +0 -5
  459. package/dist/clients/lsp/pending-aux-coverage.js +0 -251
  460. package/dist/clients/lsp/position-encoding.js +0 -61
  461. package/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  462. package/dist/clients/lsp/server.js +0 -3209
  463. package/dist/clients/lsp/session-roots.js +0 -192
  464. package/dist/clients/lsp/spawn-history.js +0 -17
  465. package/dist/clients/lsp/sync-kind.js +0 -42
  466. package/dist/clients/lsp/tsserver-sync.js +0 -332
  467. package/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  468. package/dist/clients/lsp/wait-policy/classification.js +0 -85
  469. package/dist/clients/lsp/wait-policy/index.js +0 -10
  470. package/dist/clients/lsp/wait-policy/strategies.js +0 -303
  471. package/dist/clients/lsp/watch-queue.js +0 -61
  472. package/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  473. package/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  474. package/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  475. package/dist/clients/lsp-budget.js +0 -186
  476. package/dist/clients/lsp-document-symbols.js +0 -110
  477. package/dist/clients/lsp-lazy.js +0 -9
  478. package/dist/clients/lsp-mutation.js +0 -396
  479. package/dist/clients/lsp-status.js +0 -59
  480. package/dist/clients/map-with-concurrency.js +0 -40
  481. package/dist/clients/mcp/analyze.js +0 -420
  482. package/dist/clients/mcp/host-shim.js +0 -31
  483. package/dist/clients/mcp/ipc.js +0 -335
  484. package/dist/clients/mcp/review.js +0 -151
  485. package/dist/clients/mcp/session.js +0 -412
  486. package/dist/clients/memory-sampler.js +0 -208
  487. package/dist/clients/message-end-attribution.js +0 -30
  488. package/dist/clients/metrics-client.js +0 -107
  489. package/dist/clients/metrics-history.js +0 -327
  490. package/dist/clients/middle-man-analysis.js +0 -263
  491. package/dist/clients/model-provider.js +0 -53
  492. package/dist/clients/module-report-lsp.js +0 -189
  493. package/dist/clients/module-report.js +0 -2150
  494. package/dist/clients/mutating-tool.js +0 -651
  495. package/dist/clients/mutation-attribution.js +0 -368
  496. package/dist/clients/mutation-bridge.js +0 -240
  497. package/dist/clients/ndjson-logger.js +0 -734
  498. package/dist/clients/observed-mutation-sources.js +0 -101
  499. package/dist/clients/observed-mutation.js +0 -1253
  500. package/dist/clients/oldtext-autopatch.js +0 -128
  501. package/dist/clients/opaque-mutation-scan.js +0 -496
  502. package/dist/clients/opengrep-client.js +0 -328
  503. package/dist/clients/opengrep-config.js +0 -76
  504. package/dist/clients/package-manager.js +0 -607
  505. package/dist/clients/package-root.js +0 -38
  506. package/dist/clients/partial-edit-apply.js +0 -439
  507. package/dist/clients/path-attribution-telemetry.js +0 -43
  508. package/dist/clients/path-keyed-map.js +0 -108
  509. package/dist/clients/path-utils.js +0 -1244
  510. package/dist/clients/performance-report.js +0 -331
  511. package/dist/clients/persist-debounce.js +0 -70
  512. package/dist/clients/persistent-reverify.js +0 -271
  513. package/dist/clients/php-cs-fixer-config.js +0 -119
  514. package/dist/clients/pipeline.js +0 -1425
  515. package/dist/clients/post-autofix-notice.js +0 -37
  516. package/dist/clients/print-mode.js +0 -21
  517. package/dist/clients/probe-home-state.js +0 -228
  518. package/dist/clients/process-bridge.js +0 -66
  519. package/dist/clients/process-singletons.js +0 -210
  520. package/dist/clients/process-snapshot.js +0 -68
  521. package/dist/clients/project-changes.js +0 -181
  522. package/dist/clients/project-conventions.js +0 -171
  523. package/dist/clients/project-diagnostics/cache.js +0 -137
  524. package/dist/clients/project-diagnostics/extractors.js +0 -109
  525. package/dist/clients/project-diagnostics/fresh-fetch.js +0 -650
  526. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  527. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  528. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  529. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  530. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  531. package/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  532. package/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  533. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  534. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  535. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  536. package/dist/clients/project-diagnostics/scanner.js +0 -520
  537. package/dist/clients/project-diagnostics/types.js +0 -1
  538. package/dist/clients/project-lens-config.js +0 -799
  539. package/dist/clients/project-report.js +0 -701
  540. package/dist/clients/project-scale.js +0 -237
  541. package/dist/clients/project-scan-policy.js +0 -11
  542. package/dist/clients/project-snapshot-fingerprint.js +0 -48
  543. package/dist/clients/project-snapshot-persist-worker.js +0 -20
  544. package/dist/clients/project-snapshot.js +0 -1653
  545. package/dist/clients/project-trust.js +0 -181
  546. package/dist/clients/python-environment.js +0 -217
  547. package/dist/clients/python-provenance.js +0 -639
  548. package/dist/clients/quiet-window-config.js +0 -47
  549. package/dist/clients/quiet-window.js +0 -236
  550. package/dist/clients/read-bridge.js +0 -139
  551. package/dist/clients/read-expansion.js +0 -266
  552. package/dist/clients/read-guard-logger.js +0 -251
  553. package/dist/clients/read-guard-tool-lines.js +0 -1306
  554. package/dist/clients/read-guard.js +0 -1561
  555. package/dist/clients/recent-touches.js +0 -240
  556. package/dist/clients/redact/secrets.js +0 -449
  557. package/dist/clients/resource-sampler.js +0 -628
  558. package/dist/clients/reverse-deps.js +0 -270
  559. package/dist/clients/review-graph/builder.js +0 -5118
  560. package/dist/clients/review-graph/format.js +0 -33
  561. package/dist/clients/review-graph/git-identity.js +0 -157
  562. package/dist/clients/review-graph/import-resolvers.js +0 -481
  563. package/dist/clients/review-graph/persist-worker.js +0 -10
  564. package/dist/clients/review-graph/query.js +0 -244
  565. package/dist/clients/review-graph/revision-drift.js +0 -21
  566. package/dist/clients/review-graph/service.js +0 -96
  567. package/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  568. package/dist/clients/review-graph/symbol-id.js +0 -103
  569. package/dist/clients/review-graph/tsconfig-paths.js +0 -299
  570. package/dist/clients/review-graph/types.js +0 -1
  571. package/dist/clients/review-graph/workspace-modules.js +0 -449
  572. package/dist/clients/review-graph-logger.js +0 -81
  573. package/dist/clients/ruff-client.js +0 -152
  574. package/dist/clients/rules-scanner.js +0 -131
  575. package/dist/clients/run-duration.js +0 -55
  576. package/dist/clients/runtime-agent-end.js +0 -746
  577. package/dist/clients/runtime-config.js +0 -77
  578. package/dist/clients/runtime-context.js +0 -211
  579. package/dist/clients/runtime-coordinator.js +0 -1290
  580. package/dist/clients/runtime-session.js +0 -2485
  581. package/dist/clients/runtime-tool-call.js +0 -1284
  582. package/dist/clients/runtime-tool-result.js +0 -2034
  583. package/dist/clients/runtime-turn.js +0 -3600
  584. package/dist/clients/rust-client.js +0 -97
  585. package/dist/clients/safe-spawn.js +0 -2166
  586. package/dist/clients/sanitize.js +0 -286
  587. package/dist/clients/scan-utils.js +0 -25
  588. package/dist/clients/scratch-tree-policy.js +0 -165
  589. package/dist/clients/search-read-registration.js +0 -94
  590. package/dist/clients/secret-findings.js +0 -114
  591. package/dist/clients/security-scan-client.js +0 -352
  592. package/dist/clients/session-event-guard.js +0 -297
  593. package/dist/clients/session-lifecycle.js +0 -484
  594. package/dist/clients/session-start-observability.js +0 -94
  595. package/dist/clients/session-state-store.js +0 -101
  596. package/dist/clients/session-summary.js +0 -15
  597. package/dist/clients/sessionstart-logger.js +0 -27
  598. package/dist/clients/sg-runner.js +0 -876
  599. package/dist/clients/sgconfig.js +0 -347
  600. package/dist/clients/shared-checkout-guard.js +0 -338
  601. package/dist/clients/single-flight.js +0 -158
  602. package/dist/clients/situational-tool-telemetry.js +0 -108
  603. package/dist/clients/skills-resolver.js +0 -141
  604. package/dist/clients/slow-fs.js +0 -138
  605. package/dist/clients/smells-rollup.js +0 -231
  606. package/dist/clients/source-filter.js +0 -596
  607. package/dist/clients/source-walker.js +0 -211
  608. package/dist/clients/spawn-output-cap.js +0 -37
  609. package/dist/clients/spawn-timeout-cooldown.js +0 -110
  610. package/dist/clients/stale-marker.js +0 -17
  611. package/dist/clients/startup-scan.js +0 -400
  612. package/dist/clients/startup-timing.js +0 -130
  613. package/dist/clients/string-utils.js +0 -33
  614. package/dist/clients/subagent-mode.js +0 -111
  615. package/dist/clients/symbol-containment.js +0 -24
  616. package/dist/clients/symbol-types.js +0 -5
  617. package/dist/clients/test-runner-client.js +0 -2507
  618. package/dist/clients/test-runner-delivery.js +0 -287
  619. package/dist/clients/todo-scanner.js +0 -229
  620. package/dist/clients/tool-agreement.js +0 -423
  621. package/dist/clients/tool-config.js +0 -249
  622. package/dist/clients/tool-cwd.js +0 -270
  623. package/dist/clients/tool-definition.js +0 -47
  624. package/dist/clients/tool-event.js +0 -105
  625. package/dist/clients/tool-policy.js +0 -2755
  626. package/dist/clients/tool-probe.js +0 -53
  627. package/dist/clients/tool-render.js +0 -156
  628. package/dist/clients/tool-set-policy.js +0 -118
  629. package/dist/clients/tree-sitter-cache.js +0 -364
  630. package/dist/clients/tree-sitter-client.js +0 -4134
  631. package/dist/clients/tree-sitter-logger.js +0 -100
  632. package/dist/clients/tree-sitter-navigator.js +0 -269
  633. package/dist/clients/tree-sitter-query-loader.js +0 -569
  634. package/dist/clients/tree-sitter-shared.js +0 -158
  635. package/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  636. package/dist/clients/trivy-client.js +0 -467
  637. package/dist/clients/tui-fit.js +0 -50
  638. package/dist/clients/turn-context.js +0 -52
  639. package/dist/clients/turn-summary-render.js +0 -72
  640. package/dist/clients/turn-summary.js +0 -132
  641. package/dist/clients/typos-config.js +0 -29
  642. package/dist/clients/user-notify.js +0 -51
  643. package/dist/clients/vanished-instance-marker.js +0 -57
  644. package/dist/clients/warm-attach.js +0 -246
  645. package/dist/clients/widget-state.js +0 -1677
  646. package/dist/clients/word-index-logger.js +0 -49
  647. package/dist/clients/word-index-store.js +0 -459
  648. package/dist/clients/word-index.js +0 -2080
  649. package/dist/clients/workspace-topology.js +0 -389
  650. package/dist/clients/write-ordering-guard.js +0 -65
  651. package/dist/clients/zizmor-config.js +0 -327
  652. package/dist/i18n.js +0 -69
  653. package/dist/mcp/build-staleness.js +0 -124
  654. package/dist/scripts/lib/process-scan.mjs +0 -583
  655. package/dist/scripts/lib/skills-predicate.mjs +0 -129
  656. package/dist/tools/activate-tools.js +0 -87
  657. package/dist/tools/ast-grep-outline.js +0 -137
  658. package/dist/tools/ast-grep-replace.js +0 -244
  659. package/dist/tools/ast-grep-search.js +0 -762
  660. package/dist/tools/effective-config.js +0 -90
  661. package/dist/tools/lens-diagnostic-mark.js +0 -375
  662. package/dist/tools/lens-diagnostics.js +0 -2544
  663. package/dist/tools/lsp-diagnostics.js +0 -1634
  664. package/dist/tools/lsp-navigation.js +0 -1367
  665. package/dist/tools/lsp-structured-output.js +0 -294
  666. package/dist/tools/module-report.js +0 -382
  667. package/dist/tools/project-report.js +0 -86
  668. package/dist/tools/render-compact.js +0 -374
  669. package/dist/tools/scan-progress.js +0 -53
  670. package/dist/tools/shared.js +0 -29
  671. package/dist/tools/symbol-search.js +0 -155
@@ -1,1253 +0,0 @@
1
- /**
2
- * The observational mutation net (#2430).
3
- *
4
- * ## What it is
5
- *
6
- * `clients/mutating-tool.ts` classifies a mutation by NAME or by input SHAPE.
7
- * Both tiers are finite and the population of third-party edit tools is not, so
8
- * a tool pi-lens has never met is dropped before the first bookkeeping call —
9
- * no read-guard stamp, no `turn-state.json` entry, no deferred format.
10
- *
11
- * This module makes detection OBSERVATIONAL. It watches a bounded file set
12
- * around a call the seam could not classify, and if something changed it
13
- * replays the change through the mutation bridge as a real `kind: "edit"`. The
14
- * tool is then ATTRIBUTED (`clients/mutation-attribution.ts`), so a later
15
- * session on the same project classifies it from disk with no snapshot at all.
16
- *
17
- * Three layers, cheapest first:
18
- *
19
- * 1. **Nothing at all** for a tool the seam already classifies. `arm` is never
20
- * reached: `runtime-tool-call.ts` only calls in when `classifyMutatingTool`
21
- * returned `undefined` or the attribution is still provisional, and the
22
- * first thing `arm` does is a map lookup.
23
- * 2. **Arm + diff** for an unclassified call whose input carries a path-shaped
24
- * field, bounded to THAT PATH ALONE. Paid at most a handful of times per
25
- * tool name per session (see `CLEAN_OBSERVATION_ARM_LIMIT` and
26
- * `PERSIST_AFTER_OBSERVATIONS`).
27
- * 3. **The settled sweep** at `agent_settled`, before the deferred drain, for
28
- * tools with no path field at all. It stat-checks the tracked-file set
29
- * incrementally and NEVER walks the workspace.
30
- *
31
- * ## The observation universe is the TARGET PATH (#2449 review round 2)
32
- *
33
- * The first cut snapshotted the target's whole DIRECTORY plus the tracked-file
34
- * set. That was wrong in both directions and expensive in a third:
35
- *
36
- * - it attributed a SIBLING's change to the tool under observation, so a
37
- * background write during a `read`-shaped call taught pi-lens that `read`
38
- * mutates (round-2 finding F4);
39
- * - it made the arm cost scale with directory size and tracked-set size
40
- * (~44ms warm), for a verdict about ONE path;
41
- * - it duplicated the settled sweep's job. The tracked set is the SWEEP's
42
- * domain; the armed observation's domain is the path the tool named.
43
- *
44
- * So the universe is now: the path-shaped field's file, or — when that path is
45
- * a DIRECTORY — that directory's own entries, non-recursively, capped at
46
- * {@link OBSERVED_TARGET_DIR_MAX_ENTRIES}. Nothing else. A tool that changes a
47
- * file it never named is the settled sweep's business, not the armed
48
- * observation's, and the sweep says so honestly rather than guessing.
49
- *
50
- * When that cap BITES the observation is truncated, and a truncated
51
- * observation is `unverifiable` — never clean (#2449 review round 3, S3). The
52
- * first cut broke out of the readdir loop silently, so a codemod that rewrote
53
- * the 84th entry of an 84-entry directory produced an empty diff, the empty
54
- * diff advanced the clean latch, and two of those stopped pi-lens watching a
55
- * tool that mutates on every call.
56
- *
57
- * ## What layer 3 cannot see, stated rather than hidden
58
- *
59
- * The sweep compares against a ledger seeded from files pi-lens has ALREADY
60
- * seen. A file that was never read, never written, never diagnosed and never
61
- * opened by a language server has no baseline, so its first drift only seeds
62
- * the ledger and is not reported. That is the documented limitation in #2430's
63
- * third acceptance criterion; the alternative is a workspace walk, which the
64
- * issue rules out.
65
- *
66
- * The sweep is also INCREMENTAL (round-2 finding F3). A full hash of 400
67
- * tracked files cannot finish inside a 200ms capture budget, so the first cut
68
- * timed out at every realistic size and never ran at all. It now stats a
69
- * bounded window per turn from a carried cursor, reads a file only when its
70
- * size or mtime actually moved, and reports its own coverage
71
- * (`scanned`/`remaining`/`cursor`) so a partial pass is never read as a clean
72
- * one.
73
- *
74
- * ## Bounds (AGENTS.md async rule, both directions)
75
- *
76
- * Every ASYNC step here carries a TIMEOUT and an `AbortSignal` race, and every
77
- * capture is additionally bounded by a file cap and a hash-byte budget. The
78
- * ARM shares a per-turn wall-clock budget; exhausting it emits a bounded
79
- * `observed_mutation_budget_exhausted` record and a degradation-ledger tally —
80
- * it is never a silent skip (catalog shape 10).
81
- *
82
- * The SETTLE is ASYNC and deliberately NOT budget-gated. Round 2 made it
83
- * synchronous to keep `handleToolResult` from yielding before it dispatches
84
- * the pipeline (#1086); round 3 (T4) removed the sync filesystem work from the
85
- * tool_result path instead and made the ORDER explicit at the call site: the
86
- * classified chain reads everything it derives from the post-result bytes
87
- * BEFORE the settle's yield, so a racing tool_result for the same path cannot
88
- * make this call register under the other call's state hash. Only the
89
- * pending-baseline PROBE (`hasPendingObservation`) stays synchronous, which is
90
- * what keeps the cost on the overwhelmingly common no-baseline path at one map
91
- * lookup.
92
- *
93
- * A settle clamped to a spent budget silently dropped real mutations (round-2
94
- * findings F1 and F5), so the settle has its own deadline rather than the
95
- * arm's leftovers. The snapshot already exists and the post-capture always
96
- * runs its first entry, so it completes for the target path whatever the clock
97
- * says; a directory target's remaining entries are the only part a deadline
98
- * can cut, and a cut capture says so rather than scoring itself clean.
99
- */
100
- import * as fs from "node:fs";
101
- import * as path from "node:path";
102
- import { createHash } from "node:crypto";
103
- import { BoundedFifoMap, BoundedSet } from "./bounded-cache.js";
104
- import { emitBounded } from "./bounded-telemetry.js";
105
- import { freshnessFromMtime } from "./freshness.js";
106
- import { logLatency } from "./latency-logger.js";
107
- import { noteObservedClean, noteObservedMutation, noteObservedUnverifiable, shouldArmObservationForTool, } from "./mutation-attribution.js";
108
- import { captureFileStatsForPaths, diffFileStats, } from "./opaque-mutation-scan.js";
109
- import { normalizeMapKey } from "./path-utils.js";
110
- import { getProcessSingleton } from "./process-singletons.js";
111
- import { bounded } from "./deadline-utils.js";
112
- import { lineContentHash } from "./read-guard.js";
113
- /**
114
- * Wall-clock CEILING for the ARM capture — not its cost.
115
- *
116
- * Since the universe collapsed to the target path this is one `stat`, one
117
- * optional `readdir`, and a bounded set of stat+hash pairs. The ceiling is
118
- * generous on purpose: a slow filesystem (#462 measured 1.3ms per stat on 9p)
119
- * must not turn every first observation into a timeout, which is the least
120
- * useful possible outcome.
121
- */
122
- const OBSERVED_CAPTURE_BUDGET_MS = 200;
123
- /**
124
- * Cumulative ceiling for every ARMED capture in one turn. A turn that calls
125
- * twenty unclassified tools pays this once, not twenty times. The SETTLE does
126
- * not consult it — see the module header.
127
- */
128
- export const OBSERVED_TURN_BUDGET_MS = 600;
129
- /**
130
- * Per-entry deadline for the settle's re-capture.
131
- *
132
- * Tighter than {@link OBSERVED_CAPTURE_BUDGET_MS} because the settle sits on
133
- * the `tool_result` path, between a tool finishing and pi-lens dispatching its
134
- * pipeline: every millisecond here is latency the agent waits through. Measured
135
- * on this repo: a file target settles in ~1.4ms and a full 42-entry DIRECTORY
136
- * target in ~20ms, so this clears the realistic worst case and only bites on a
137
- * filesystem far slower than the one it was measured on.
138
- *
139
- * It can never cut the TARGET itself — `captureFileStatsForPaths` always runs
140
- * its first entry — so F5's "the settle always completes for the target path"
141
- * holds regardless of the clock. What it CAN cut is a directory target's tail,
142
- * and that is reported as `stoppedEarly` rather than scored as clean.
143
- */
144
- const OBSERVED_SETTLE_DEADLINE_MS = 50;
145
- /**
146
- * Entries taken from a DIRECTORY-shaped target path, non-recursively.
147
- *
148
- * A tool that names a directory is saying "I operate in here"; its own entries
149
- * are the honest universe. Sixty-four is a blast-radius bound, not a guess
150
- * about directory sizes: past it the observation degrades to the entries it
151
- * did see, and the settled sweep remains the net for the rest.
152
- */
153
- export const OBSERVED_TARGET_DIR_MAX_ENTRIES = 64;
154
- /** Tracked files (read-guard + widget + open LSP docs) the sweep may hold. */
155
- export const OBSERVED_TRACKED_MAX_FILES = 400;
156
- /**
157
- * How long a file must have been QUIET before its ledger entry may be trusted
158
- * for the sweep's stat short-circuit.
159
- *
160
- * Its own constant, not `OPAQUE_MTIME_TOLERANCE_MS` (#2449 review round 3,
161
- * S1). That one answers "how far before a recorded start may an earlier write
162
- * still be attributed to this call" — an attribution window. This one answers
163
- * "was the file still being written when we took its baseline" — a settling
164
- * window. They happen to share a number today; a change to either for its own
165
- * reasons must not move the other.
166
- *
167
- * ## The direction it does NOT close, stated
168
- *
169
- * On a filesystem whose mtime granularity is COARSER than this window (FAT's
170
- * two seconds, HFS+'s one), a baseline recorded 150ms or more into a tick and
171
- * a same-size rewrite later in that SAME tick still short-circuit: the entry
172
- * looks settled because the mtime it carries is old, and the rewrite does not
173
- * move it. That residual window is the price of the short-circuit that makes
174
- * the sweep affordable at all. The settled sweep's next pass over the file
175
- * closes it as soon as anything moves the size or the tick, and the armed
176
- * observation (which hashes the target unconditionally) never depended on it.
177
- */
178
- const OBSERVED_LEDGER_SETTLE_MS = 150;
179
- /**
180
- * Path keys the "pi-lens already recorded this" set may hold.
181
- *
182
- * Marks are normally retired per file by {@link refreshObservedMutationLedger},
183
- * as it re-baselines each one. A refresh that keeps parking before it reaches
184
- * the tail leaves the tail's marks standing indefinitely, so the set still needs
185
- * a bound of its own — and, because dropping a mark is not free (it makes the
186
- * next sweep read pi-lens's own bytes as third-party drift), the drop emits
187
- * `observed_handled_evicted` naming the path (#2449 review round 4, S2).
188
- */
189
- export const OBSERVED_HANDLED_MAX = 1000;
190
- /**
191
- * Files the settled sweep STATS in one turn before parking its cursor.
192
- *
193
- * Smaller than {@link OBSERVED_TRACKED_MAX_FILES} on purpose: coverage of the
194
- * whole tracked set is spread across turns rather than attempted (and timed
195
- * out) in one. The read-guard's own set is ordered first by
196
- * `collectTrackedPaths`, so the files the agent is actually working on are at
197
- * the front of the rotation.
198
- */
199
- export const OBSERVED_SWEEP_STAT_WINDOW = 128;
200
- /** Cumulative content-hash budget for ONE capture. */
201
- const OBSERVED_HASH_BUDGET_BYTES = 2 * 1024 * 1024;
202
- /** Cumulative bytes ONE settled sweep may read to seed or verify a hash. */
203
- export const OBSERVED_SWEEP_HASH_BUDGET_BYTES = 2 * 1024 * 1024;
204
- /** Cumulative bytes ONE settled sweep may read to derive edit RANGES. */
205
- const OBSERVED_SWEEP_RANGE_BUDGET_BYTES = 1024 * 1024;
206
- /** Largest file whose per-line hashes are captured for range derivation. */
207
- const OBSERVED_LINE_HASH_MAX_BYTES = 512 * 1024;
208
- /** Ranges reported per file before they collapse to one bounding box. */
209
- const OBSERVED_MAX_EDIT_RANGES = 32;
210
- /** Pending baselines held between `tool_call` and `tool_result`. */
211
- const OBSERVED_PENDING_MAX = 32;
212
- /** Files remembered by the settled-sweep content ledger. */
213
- const OBSERVED_LEDGER_MAX = 1000;
214
- const OBSERVED_FAMILY = "observed-mutation-net";
215
- const OBSERVED_VERSION = 3;
216
- function state() {
217
- return getProcessSingleton(OBSERVED_FAMILY, OBSERVED_VERSION, () => ({
218
- pending: new BoundedFifoMap(OBSERVED_PENDING_MAX),
219
- ledger: new BoundedFifoMap(OBSERVED_LEDGER_MAX),
220
- handled: new BoundedSet(OBSERVED_HANDLED_MAX),
221
- turnIndex: -1,
222
- turnSpentMs: 0,
223
- sweepCursor: 0,
224
- }));
225
- }
226
- /**
227
- * Session boundary (#2430). Pending baselines are keyed by tool-call id and
228
- * are unreachable once the session generation advances; the content ledger and
229
- * the handled set describe a finished session's files. All of it must clear or
230
- * a resumed session diffs against another session's world.
231
- */
232
- export function resetObservedMutationNet() {
233
- const current = state();
234
- current.pending.clear();
235
- current.ledger.clear();
236
- current.handled.clear();
237
- current.turnIndex = -1;
238
- current.turnSpentMs = 0;
239
- current.sweepCursor = 0;
240
- }
241
- /** Test seam: the net's live state, as plain data. */
242
- export function _observedMutationStateForTests() {
243
- const current = state();
244
- return {
245
- pending: [...current.pending.keys()],
246
- ledger: [...current.ledger.keys()],
247
- handled: [...current.handled],
248
- turnSpentMs: current.turnSpentMs,
249
- sweepCursor: current.sweepCursor,
250
- };
251
- }
252
- /**
253
- * Remember that the normal pipeline already recorded this path this run, so the
254
- * settled sweep refreshes its baseline instead of reporting the same bytes a
255
- * second time. Called from the classified `tool_result` path and from
256
- * `recordMutationThroughSeam`, which is every in-process producer.
257
- */
258
- export function noteMutationHandled(filePath) {
259
- try {
260
- const handled = state().handled;
261
- const key = normalizeMapKey(path.resolve(filePath));
262
- // `BoundedSet#add` (#2460) owns the FIFO eviction and hands back what it
263
- // dropped, oldest first — `handled` is membership-only, so there is never
264
- // more than one entry here, but the array shape keeps this call site
265
- // identical to every other `BoundedFifoMap`/`BoundedSet` eviction
266
- // consumer rather than assuming the cardinality.
267
- const evicted = handled.add(key);
268
- for (const oldest of evicted) {
269
- // The drop is NOT silent (#2449 review round 4, S2). Dropping a mark
270
- // reinstates exactly the defect round 3 (S5) fixed: the ledger still
271
- // holds the PRE-drain bytes for this file while the only record that
272
- // those bytes were pi-lens's own has just been thrown away, so the
273
- // next settled sweep replays our own formatter output as third-party
274
- // drift. Naming the victim makes that a traceable record rather than a
275
- // mystery re-format (catalog shape 10).
276
- emitBounded("observed_handled_evicted",
277
- // Identity is the DROPPED PATH, not a constant label: the ledger
278
- // entry is keyed by subject and survives the per-turn cap on the
279
- // detailed record, so it is what still names WHICH file lost its
280
- // mark after a turn that overflowed the set many times.
281
- oldest, {
282
- filePath: oldest,
283
- durationMs: 0,
284
- result: `cap:${OBSERVED_HANDLED_MAX}`,
285
- }, {
286
- ledgerKind: "observed-mutation-budget",
287
- reason: `the handled set is full at ${OBSERVED_HANDLED_MAX}; the oldest pi-lens-authored file lost its mark, so its next drift is reported as third-party`,
288
- capPerTurn: { limit: 2, turnIndex: state().turnIndex },
289
- });
290
- }
291
- }
292
- catch {
293
- // A path that cannot be resolved cannot collide with a ledger key either.
294
- }
295
- }
296
- /**
297
- * Whether a `tool_result` has a baseline waiting for it.
298
- *
299
- * SYNCHRONOUS, and the first thing the `tool_result` seam asks (#2449 review
300
- * round 2, F1). The previous cut awaited the settle unconditionally, so every
301
- * tool_result — including the overwhelming majority that miss this map —
302
- * yielded a microtask before `handleToolResult` reached its debounce and
303
- * in-flight registration, breaking the #1086 ordering contract.
304
- */
305
- export function hasPendingObservation(toolCallId) {
306
- return toolCallId !== undefined && state().pending.has(toolCallId);
307
- }
308
- function remainingTurnBudgetMs(turnIndex) {
309
- const current = state();
310
- if (current.turnIndex !== turnIndex) {
311
- current.turnIndex = turnIndex;
312
- current.turnSpentMs = 0;
313
- }
314
- return Math.max(0, OBSERVED_TURN_BUDGET_MS - current.turnSpentMs);
315
- }
316
- function chargeTurnBudget(turnIndex, spentMs) {
317
- const current = state();
318
- if (current.turnIndex !== turnIndex) {
319
- current.turnIndex = turnIndex;
320
- current.turnSpentMs = 0;
321
- }
322
- current.turnSpentMs += Math.max(0, spentMs);
323
- }
324
- /** Test seam: force the per-turn budget to a known state. */
325
- export function _setObservedTurnBudgetForTests(turnIndex, spentMs) {
326
- const current = state();
327
- current.turnIndex = turnIndex;
328
- current.turnSpentMs = spentMs;
329
- }
330
- /**
331
- * Both bounds on one async step: a wall-clock timeout AND an abort race.
332
- *
333
- * #2523 slice 2 folded the hand-rolled timer race out of here and onto
334
- * `bounded()`. What is left is the ADAPTER: this seam needs a three-way
335
- * outcome (`timeout` / `aborted` / `failed`) that `bounded()` deliberately
336
- * does not hand back, because a throw and a blown budget have different
337
- * remedies and folding them together is catalog shape 10.
338
- *
339
- * A loser is DISCARDED, never awaited to completion — the underlying work is
340
- * stat/read only, so letting it finish unobserved costs nothing, while awaiting
341
- * it would defeat the bound this exists to enforce.
342
- *
343
- * `T extends object` is the load-bearing constraint, not decoration. `bounded()`
344
- * spells "a bound fired" as a bare `undefined`, so a `work` that could itself
345
- * resolve `undefined` would be indistinguishable from a timeout here. The first
346
- * cut paid for that with a `.then((value) => ({ value }))` box — which was
347
- * VACUOUS: all four call sites return object literals, so no mutation of the box
348
- * could red a test, and the comment justifying it named `captureLineHashes`,
349
- * which returns a record and not `undefined` (#2557 review F6). Constraining the
350
- * type parameter makes the invariant the box was pretending to protect a COMPILE
351
- * error at any future call site instead of a per-call allocation.
352
- */
353
- async function withBounds(work, timeoutMs, signal, site) {
354
- // A function, not an inline read: the signal is LIVE, so the compiler's
355
- // narrowing from the pre-flight check below must not be carried across the
356
- // await into the post-settle classification (it would fold that branch to
357
- // "timeout" and hide every mid-await abort).
358
- const isAborted = () => signal !== undefined && signal.aborted;
359
- if (isAborted())
360
- return { ok: false, reason: "aborted" };
361
- try {
362
- const settled = await bounded(work(), {
363
- ms: timeoutMs,
364
- // The observational net runs on hook paths that may or may not carry
365
- // a signal; the wall budget is the bound that is always there, and
366
- // `bounded()` reads a missing signal as one that never aborts.
367
- signal,
368
- hook: site.hook,
369
- label: site.label,
370
- });
371
- if (settled !== undefined)
372
- return { ok: true, value: settled };
373
- // `bounded()` applies the caller's signal FIRST, so reading it back here
374
- // reproduces which bound fired without a second channel.
375
- return { ok: false, reason: isAborted() ? "aborted" : "timeout" };
376
- }
377
- catch {
378
- // A THROW gets its own reason. Folding it into `timeout` is exactly the
379
- // misclassification catalog shape 10 warns about: a reader tuning the
380
- // budget would be chasing a bug that has nothing to do with time.
381
- return { ok: false, reason: "failed" };
382
- }
383
- }
384
- /**
385
- * Test-only alias so `tests/clients/hook-await-fold-bounds.test.ts` can pin
386
- * `withBounds`'s `T extends object` constraint at compile time (#2557 review
387
- * F-B). `withBounds` itself is module-private -- there is no other way for a
388
- * test file to reference it in a `@ts-expect-error` probe. Never called
389
- * outside that probe.
390
- */
391
- export const _withBoundsForTests = withBounds;
392
- /** `splitLines` semantics from `read-guard.ts`, kept identical on purpose. */
393
- function splitLines(text) {
394
- return text.split(/\r?\n/);
395
- }
396
- async function captureLineHashes(filePath, budget) {
397
- try {
398
- const size = (await fs.promises.stat(filePath)).size;
399
- if (size > OBSERVED_LINE_HASH_MAX_BYTES)
400
- return undefined;
401
- if (budget) {
402
- if (budget.remainingBytes < size)
403
- return undefined;
404
- budget.remainingBytes -= size;
405
- }
406
- const lines = splitLines(await fs.promises.readFile(filePath, "utf-8"));
407
- const hashes = new Map();
408
- for (let index = 0; index < lines.length; index += 1) {
409
- hashes.set(index + 1, lineContentHash(lines[index] ?? ""));
410
- }
411
- return hashes;
412
- }
413
- catch {
414
- return undefined;
415
- }
416
- }
417
- /**
418
- * A baseline's per-line hashes as a dense 1..N array, or `undefined` when it
419
- * does not cover the whole file.
420
- *
421
- * The read-guard stores hashes for the lines a read actually SHOWED, so a
422
- * windowed read's map covers a slice (say lines 60..100) and says nothing at
423
- * all about the rest. Treating that as a whole-file baseline is what made the
424
- * first cut drop a real change at line 3 and report a fabricated 61..101 range
425
- * (#2449 review round 2, F6). A baseline is usable only when its keys are
426
- * exactly 1..N.
427
- */
428
- function denseLineBaseline(before) {
429
- if (before === undefined)
430
- return undefined;
431
- const entries = before instanceof Map
432
- ? [...before.entries()]
433
- : Object.entries(before).map(([line, hash]) => [Number(line), hash]);
434
- const count = entries.length;
435
- if (count === 0)
436
- return undefined;
437
- const ordered = [];
438
- ordered.length = count;
439
- for (const [line, hash] of entries) {
440
- if (!Number.isInteger(line) || line < 1 || line > count)
441
- return undefined;
442
- ordered[line - 1] = hash;
443
- }
444
- for (const value of ordered)
445
- if (value === undefined)
446
- return undefined;
447
- return ordered;
448
- }
449
- /**
450
- * Line ranges that actually differ, from the same FNV-1a whitespace-stripped
451
- * per-line hash the read-guard stores for every read (#505). `before` is the
452
- * pre-call capture when the net armed one, and otherwise the read-guard's own
453
- * stored hashes for the file — the issue's "content diff against the
454
- * read-guard's stored content".
455
- *
456
- * Returns `undefined` — meaning "no ranges, over-approximate to the whole
457
- * file" — whenever a per-line comparison would be a fabrication rather than a
458
- * measurement:
459
- *
460
- * - no baseline at all;
461
- * - a PARTIAL (windowed) baseline, which knows nothing about the lines outside
462
- * its window;
463
- * - a changed LINE COUNT, where every line after an insert or delete shifts and
464
- * comparing by line number reports the shift instead of the edit;
465
- * - a file too large to hash, or a spent read budget.
466
- *
467
- * Every one of those is the safe direction: the bridge's `resolveChangedRange`
468
- * then over-approximates to the whole file rather than naming lines that were
469
- * never touched.
470
- */
471
- export async function deriveObservedEditRanges(filePath, before, budget) {
472
- const baseline = denseLineBaseline(before);
473
- if (baseline === undefined)
474
- return undefined;
475
- const after = await captureLineHashes(filePath, budget);
476
- if (after === undefined)
477
- return undefined;
478
- if (after.size !== baseline.length)
479
- return undefined;
480
- const changed = [];
481
- for (let line = 1; line <= baseline.length; line += 1) {
482
- if (after.get(line) !== baseline[line - 1])
483
- changed.push(line);
484
- }
485
- if (changed.length === 0)
486
- return undefined;
487
- const ranges = [];
488
- let start = changed[0];
489
- let end = changed[0];
490
- for (const line of changed.slice(1)) {
491
- if (line === end + 1) {
492
- end = line;
493
- continue;
494
- }
495
- ranges.push([start, end]);
496
- start = line;
497
- end = line;
498
- }
499
- ranges.push([start, end]);
500
- if (ranges.length > OBSERVED_MAX_EDIT_RANGES) {
501
- // A rewrite this scattered is a whole-file change in practice; one
502
- // bounding box keeps the record bounded (AGENTS.md bounded-record rule).
503
- return [[ranges[0][0], ranges[ranges.length - 1][1]]];
504
- }
505
- return ranges;
506
- }
507
- function boundingBox(ranges) {
508
- return [
509
- Math.min(...ranges.map(([start]) => start)),
510
- Math.max(...ranges.map(([, end]) => end)),
511
- ];
512
- }
513
- /**
514
- * Both of these are one-liners over {@link BoundedFifoMap} (#2442, adopted in
515
- * #2449 review round 4, B2). They stay as named functions because the CAP each
516
- * map carries is part of this module's contract and the call sites read
517
- * better naming the map than the container; the eviction block itself is the
518
- * primitive's.
519
- */
520
- function putPending(key, value) {
521
- state().pending.set(key, value);
522
- }
523
- function putLedger(key, value) {
524
- state().ledger.set(key, value);
525
- }
526
- function seedLedger(snapshot) {
527
- const seenAtMs = Date.now();
528
- for (const [key, entry] of snapshot) {
529
- putLedger(key, {
530
- hash: entry.hash,
531
- hashKind: entry.hash === undefined ? undefined : "content",
532
- size: entry.size,
533
- mtimeMs: entry.mtimeMs,
534
- seenAtMs,
535
- });
536
- }
537
- }
538
- function linesDigest(ordered) {
539
- return createHash("sha256").update(ordered.join("\n")).digest("hex");
540
- }
541
- function linesDigestOfContent(content) {
542
- return linesDigest(splitLines(content).map((line) => lineContentHash(line)));
543
- }
544
- /**
545
- * Take the pre-call baseline for an unclassified tool call.
546
- *
547
- * Cost on a call this does NOT arm is one `Map` lookup — the eligibility check
548
- * runs before any filesystem work, so a classified tool never reaches here at
549
- * all and a latched tool stops after the lookup.
550
- */
551
- export async function armObservedMutation(args) {
552
- if (!shouldArmObservationForTool(args.toolName))
553
- return { armed: false, reason: "not-eligible" };
554
- if (!args.toolCallId)
555
- return { armed: false, reason: "no-tool-call-id" };
556
- const remaining = remainingTurnBudgetMs(args.turnIndex);
557
- if (remaining <= 0) {
558
- emitBounded("observed_mutation_budget_exhausted", args.toolName, {
559
- filePath: args.targetPath,
560
- durationMs: 0,
561
- result: `turn-budget:${OBSERVED_TURN_BUDGET_MS}ms`,
562
- }, {
563
- ledgerKind: "observed-mutation-budget",
564
- reason: "per-turn observational snapshot budget exhausted",
565
- capPerTurn: { limit: 2, turnIndex: args.turnIndex },
566
- });
567
- return { armed: false, reason: "budget-exhausted" };
568
- }
569
- const started = Date.now();
570
- const timeoutMs = Math.min(remaining, OBSERVED_CAPTURE_BUDGET_MS);
571
- const outcome = await withBounds(async () => {
572
- const universe = await collectObservationUniverse(args.targetPath);
573
- const captured = await captureFileStatsForPaths(universe.paths, {
574
- withHashes: true,
575
- hashBudgetBytes: OBSERVED_HASH_BUDGET_BYTES,
576
- });
577
- // INSIDE the bounds, and therefore inside the turn charge below
578
- // (#2449 review round 4, S3). This reads and per-line-hashes the target
579
- // up to OBSERVED_LINE_HASH_MAX_BYTES — half a megabyte, and the
580
- // dominant cost of arming a large target. The first cut awaited it
581
- // AFTER `chargeTurnBudget` and outside `withBounds`, so the majority of
582
- // the arm was charged to nobody and covered by neither the timeout nor
583
- // the abort race — both halves of AGENTS.md's two-bounds rule missing on
584
- // the single most expensive step.
585
- const lineHashes = await captureLineHashes(args.targetPath);
586
- return {
587
- paths: universe.paths,
588
- capped: universe.capped,
589
- stats: captured.snapshot,
590
- lineHashes,
591
- };
592
- }, timeoutMs, args.signal,
593
- // Reached from `clients/runtime-tool-call.ts`, which #2523's contract
594
- // gives no wall budget of its own — the ledger key still names it so a
595
- // blown capture budget is attributable.
596
- { hook: "tool_call", label: "armObservedMutation" });
597
- chargeTurnBudget(args.turnIndex, Date.now() - started);
598
- if (!outcome.ok) {
599
- emitBounded("observed_mutation_budget_exhausted", args.toolName, {
600
- filePath: args.targetPath,
601
- durationMs: Date.now() - started,
602
- result: outcome.reason,
603
- }, {
604
- ledgerKind: "observed-mutation-budget",
605
- reason: `observational pre-snapshot ${outcome.reason}`,
606
- capPerTurn: { limit: 2, turnIndex: args.turnIndex },
607
- });
608
- return { armed: false, reason: outcome.reason };
609
- }
610
- const targetKey = normalizeMapKey(path.resolve(args.targetPath));
611
- seedLedger(outcome.value.stats);
612
- if (outcome.value.capped) {
613
- // A truncated universe is a real coverage gap and it is named here, at
614
- // the moment it happens, so it is counted even for a call whose settle
615
- // never arrives (catalog shape 10).
616
- emitBounded("observed_target_dir_capped", args.toolName, {
617
- filePath: args.targetPath,
618
- durationMs: Date.now() - started,
619
- result: `entries:${OBSERVED_TARGET_DIR_MAX_ENTRIES}`,
620
- }, {
621
- ledgerKind: "observed-mutation-dir-cap",
622
- reason: `directory target has more than ${OBSERVED_TARGET_DIR_MAX_ENTRIES} entries; the observation covers only the first ${OBSERVED_TARGET_DIR_MAX_ENTRIES}`,
623
- capPerTurn: { limit: 2, turnIndex: args.turnIndex },
624
- });
625
- }
626
- putPending(args.toolCallId, {
627
- toolName: args.toolName,
628
- startedAt: started,
629
- cwd: args.cwd,
630
- sessionGeneration: args.sessionGeneration,
631
- paths: outcome.value.paths,
632
- stats: outcome.value.stats,
633
- targetKey,
634
- targetLineHashes: outcome.value.lineHashes,
635
- targetDirCapped: outcome.value.capped,
636
- });
637
- const durationMs = Date.now() - started;
638
- logLatency({
639
- type: "phase",
640
- toolName: args.toolName,
641
- phase: "observed_mutation_prescan",
642
- filePath: args.targetPath,
643
- durationMs,
644
- result: `scanned:${outcome.value.stats.size}`,
645
- });
646
- return { armed: true, scannedCount: outcome.value.stats.size, durationMs };
647
- }
648
- /**
649
- * The snapshot universe: the TARGET PATH, and nothing else.
650
- *
651
- * A file target is itself. A DIRECTORY target is its own entries,
652
- * non-recursively and capped — a tool naming a directory is claiming that
653
- * directory as its working set. A path that does not exist yet is still
654
- * watched as itself, so a call that CREATES it is observed.
655
- *
656
- * `capped` is the load-bearing half of the return (#2449 review round 3, S3):
657
- * when the cap bites, the list below is a TRUNCATION of what the tool named,
658
- * and the settle must not read an empty diff over it as evidence the tool
659
- * changed nothing.
660
- *
661
- * See the module header for why the sibling walk and the tracked-set fold this
662
- * replaces were both wrong.
663
- */
664
- async function collectObservationUniverse(targetPath) {
665
- const target = path.resolve(targetPath);
666
- try {
667
- const stat = await fs.promises.stat(target);
668
- if (stat.isDirectory()) {
669
- const entries = await fs.promises.readdir(target, {
670
- withFileTypes: true,
671
- });
672
- const files = [];
673
- let capped = false;
674
- for (const entry of entries) {
675
- if (files.length >= OBSERVED_TARGET_DIR_MAX_ENTRIES) {
676
- capped = true;
677
- break;
678
- }
679
- if (entry.isFile())
680
- files.push(path.join(target, entry.name));
681
- }
682
- return { paths: files, capped };
683
- }
684
- }
685
- catch {
686
- // Does not exist yet, or is unreadable. Watching the path itself is the
687
- // right answer for both: a call that creates it shows up as an addition.
688
- }
689
- return { paths: [target], capped: false };
690
- }
691
- /**
692
- * Diff the post-call state against the baseline and replay what changed.
693
- *
694
- * A change here is the FIRST-CALL coverage #2430's first acceptance criterion
695
- * asks for: the tool is unknown, so nothing downstream would have recorded the
696
- * file, and this replay is what puts it in `turn-state.json`.
697
- *
698
- * ASYNC since round 3 (T4). Round 2 made it synchronous because
699
- * `handleToolResult` may not yield before dispatching the pipeline (#1086) —
700
- * but the fix for that is to keep the PROBE synchronous ({@link
701
- * hasPendingObservation}, which every call pays and almost every call fails)
702
- * and to have the caller read what it derives from the post-result bytes
703
- * BEFORE this yield. Blocking the event loop on a directory's worth of
704
- * `readFileSync` to buy an ordering property the call site can state directly
705
- * was the wrong trade. Reaching this function at all already means there is
706
- * real work to do.
707
- *
708
- * NOT budget-gated. The previous cut clamped the post-capture to whatever was
709
- * left of the per-turn arm budget, which on a busy turn is 1ms — long enough to
710
- * stat nothing and report a timeout, dropping a mutation that had already been
711
- * measured (round 2, F5). The baseline exists; re-capturing the target it was
712
- * taken for is not optional.
713
- */
714
- export async function settleObservedMutation(args) {
715
- const key = args.toolCallId;
716
- if (!key)
717
- return {
718
- settled: false,
719
- changedPaths: [],
720
- unverifiablePaths: [],
721
- replayed: 0,
722
- scanned: 0,
723
- stoppedEarly: false,
724
- reason: "no-tool-call-id",
725
- };
726
- const pending = state().pending.get(key);
727
- if (!pending) {
728
- // A missing baseline is a real answer, not a no-op: the arm was evicted
729
- // by the pending cap, or never ran. Saying so keeps "nothing changed"
730
- // distinguishable from "nothing was watched" (catalog shape 10).
731
- return {
732
- settled: false,
733
- changedPaths: [],
734
- unverifiablePaths: [],
735
- replayed: 0,
736
- scanned: 0,
737
- stoppedEarly: false,
738
- reason: "no-pending-baseline",
739
- };
740
- }
741
- state().pending.delete(key);
742
- if (pending.sessionGeneration !== args.sessionGeneration) {
743
- // Catalog shape 22: the baseline belongs to a session that has since
744
- // ended. Diffing across that boundary would attribute another session's
745
- // world to this call.
746
- return {
747
- settled: false,
748
- changedPaths: [],
749
- unverifiablePaths: [],
750
- replayed: 0,
751
- scanned: 0,
752
- stoppedEarly: false,
753
- reason: "session-generation-advanced",
754
- };
755
- }
756
- const started = Date.now();
757
- // The target is `paths[0]` for a file target and the whole (already capped)
758
- // entry list for a directory one. The deadline can only ever cut a directory
759
- // target's tail — `captureFileStatsForPaths` always runs its first entry.
760
- // The outer race exists only to bound a single wedged `stat`, so its budget
761
- // is deliberately slack compared with the per-entry deadline that does the
762
- // real work.
763
- const capture = await withBounds(() => captureFileStatsForPaths(pending.paths, {
764
- withHashes: true,
765
- hashBudgetBytes: OBSERVED_HASH_BUDGET_BYTES,
766
- deadlineMs: started + OBSERVED_SETTLE_DEADLINE_MS,
767
- signal: args.signal,
768
- }), OBSERVED_SETTLE_DEADLINE_MS * 4, args.signal, { hook: "tool_result_edit", label: "settleObservedMutation" });
769
- chargeTurnBudget(args.turnIndex, Date.now() - started);
770
- if (!capture.ok) {
771
- // A wedged filesystem call. There is no diff to report and, critically,
772
- // no evidence the tool was clean — so the clean latch is not advanced.
773
- noteObservedUnverifiable(args.toolName);
774
- return {
775
- settled: false,
776
- changedPaths: [],
777
- unverifiablePaths: [],
778
- replayed: 0,
779
- scanned: 0,
780
- stoppedEarly: true,
781
- reason: capture.reason,
782
- };
783
- }
784
- const captured = capture.value;
785
- seedLedger(captured.snapshot);
786
- const diff = diffObservedStats(pending.stats, captured.snapshot);
787
- const changed = diff.changed.filter((candidate) => args.isRecordable?.(candidate) !== false);
788
- const unverifiablePaths = diff.unverifiable.filter((candidate) => args.isRecordable?.(candidate) !== false);
789
- // Two different ways the observation can fall short of what the tool named:
790
- // the ARM already truncated a wide directory (#2449 round 3, S3), or the
791
- // re-capture's own deadline cut its tail. Both mean the same thing to the
792
- // verdict below.
793
- const truncated = pending.targetDirCapped || captured.stoppedEarly;
794
- const cutReason = pending.targetDirCapped
795
- ? "target-dir-cap-exceeded"
796
- : captured.stoppedEarly
797
- ? "capture-cut-short"
798
- : undefined;
799
- if (unverifiablePaths.length > 0) {
800
- // Hashless evidence is a bounded coverage gap, not a mutation. Keep it
801
- // visible through the existing degradation record without replaying it.
802
- noteObservedUnverifiable(args.toolName);
803
- logLatency({
804
- type: "phase",
805
- toolName: args.toolName,
806
- phase: "observed_mutation_coverage_unknown",
807
- filePath: unverifiablePaths.slice(0, 5).join(","),
808
- durationMs: Date.now() - started,
809
- result: `unverifiable:${unverifiablePaths.length}`,
810
- });
811
- }
812
- if (changed.length === 0) {
813
- // An INCOMPLETE observation is not evidence of cleanliness — it is
814
- // evidence we stopped looking. Advancing the clean latch on it would
815
- // teach pi-lens to stop watching a tool it never finished watching, and
816
- // with a directory wider than the cap that is every single call.
817
- if (truncated)
818
- noteObservedUnverifiable(args.toolName);
819
- else if (unverifiablePaths.length === 0)
820
- noteObservedClean(args.toolName);
821
- return {
822
- settled: true,
823
- changedPaths: [],
824
- unverifiablePaths,
825
- replayed: 0,
826
- scanned: captured.snapshot.size,
827
- stoppedEarly: truncated,
828
- reason: cutReason,
829
- };
830
- }
831
- const rangeBudget = {
832
- remainingBytes: OBSERVED_HASH_BUDGET_BYTES,
833
- };
834
- let replayed = 0;
835
- for (const filePath of changed) {
836
- const baseline = filePath === pending.targetKey
837
- ? pending.targetLineHashes
838
- : args.getStoredLineHashes?.(filePath);
839
- const editRanges = await deriveObservedEditRanges(filePath, baseline, rangeBudget);
840
- const accepted = args.record({
841
- filePath,
842
- kind: "edit",
843
- touchedLines: editRanges ? boundingBox(editRanges) : undefined,
844
- editRanges: editRanges && editRanges.length > 1 ? editRanges : undefined,
845
- consumer: args.toolName,
846
- provenance: "observed",
847
- });
848
- if (accepted) {
849
- replayed += 1;
850
- noteMutationHandled(filePath);
851
- }
852
- }
853
- if (replayed > 0) {
854
- // The universe IS the target path, so a replay here means the tool
855
- // changed what it named — never a sibling that happened to move
856
- // underneath it (#2449 round 2, F4).
857
- const attribution = noteObservedMutation(args.toolName, pending.cwd);
858
- logLatency({
859
- type: "phase",
860
- toolName: args.toolName,
861
- phase: "observed_mutation_recovered",
862
- filePath: changed.slice(0, 5).join(","),
863
- durationMs: Date.now() - started,
864
- result: `changed:${changed.length} observations:${attribution.observations}${attribution.persisted ? " persisted" : ""}`,
865
- });
866
- }
867
- else {
868
- // Every candidate was refused by the recorder (out of scope, or the
869
- // bookkeeping failed). That is not evidence the tool is clean, so the
870
- // clean latch is deliberately NOT advanced here.
871
- logLatency({
872
- type: "phase",
873
- toolName: args.toolName,
874
- phase: "observed_mutation_coverage_unknown",
875
- filePath: changed.slice(0, 5).join(","),
876
- durationMs: Date.now() - started,
877
- result: `refused:${changed.length}`,
878
- });
879
- }
880
- return {
881
- settled: true,
882
- changedPaths: changed,
883
- unverifiablePaths,
884
- replayed,
885
- scanned: captured.snapshot.size,
886
- stoppedEarly: truncated,
887
- reason: cutReason,
888
- };
889
- }
890
- /**
891
- * The observational net asks whether to look harder. An absent content hash
892
- * therefore remains unverifiable even when cheap stat fields report a change.
893
- * #2984: stat-only evidence must not grant ownership or replay a mutation.
894
- */
895
- function diffObservedStats(before, after) {
896
- const changed = new Set(diffFileStats(before, after));
897
- const unverifiable = new Set();
898
- for (const [key, stat] of after) {
899
- const previous = before.get(key);
900
- if (previous && (previous.hash === undefined || stat.hash === undefined)) {
901
- changed.delete(key);
902
- unverifiable.add(key);
903
- }
904
- }
905
- return { changed: [...changed], unverifiable: [...unverifiable] };
906
- }
907
- async function readBytesSafe(filePath) {
908
- try {
909
- return await fs.promises.readFile(filePath);
910
- }
911
- catch {
912
- return undefined;
913
- }
914
- }
915
- /**
916
- * One incremental pass over the tracked set (#2449 round 2, F3).
917
- *
918
- * The shape that makes this affordable: **stat first, read only on change.**
919
- * A file whose size and mtime match the ledger costs one `stat` and nothing
920
- * else, which is the steady state for almost every file on almost every turn.
921
- * A file whose stat moved is the only one worth reading, and that read is what
922
- * separates a real edit from a `touch`.
923
- *
924
- * A first sighting prefers the read-guard's stored per-line hashes over a file
925
- * read: #505 already paid for those bytes, so seeding a file the agent has read
926
- * costs no I/O beyond the stat it already did.
927
- *
928
- * `report: false` is the post-drain re-baseline. Its traversal is NOT the
929
- * tracked set — see `refreshObservedMutationLedger` for why it walks
930
- * `handled` instead — so `getTrackedPaths` is only ever consulted on the
931
- * `report: true` (settled-sweep) path and is optional here.
932
- */
933
- async function scanTrackedIncrementally(args, options) {
934
- const current = state();
935
- let tracked;
936
- if (options.report) {
937
- try {
938
- tracked = (args.getTrackedPaths?.() ?? []).slice(0, OBSERVED_TRACKED_MAX_FILES);
939
- }
940
- catch {
941
- tracked = [];
942
- }
943
- }
944
- else {
945
- // The post-drain refresh's job is to re-baseline pi-lens's OWN drain
946
- // output, and every file the drain wrote this run is already in
947
- // `handled` (see `noteMutationHandled`) — so THAT is the traversal, not
948
- // the full tracked set. See `refreshObservedMutationLedger` for the
949
- // coverage argument.
950
- tracked = [...current.handled];
951
- }
952
- const total = tracked.length;
953
- if (total === 0) {
954
- current.sweepCursor = 0;
955
- return {
956
- scanned: 0,
957
- tracked: 0,
958
- cursor: 0,
959
- drifted: [],
960
- unverifiable: [],
961
- stoppedEarly: false,
962
- };
963
- }
964
- const startCursor = options.report
965
- ? ((current.sweepCursor % total) + total) % total
966
- : 0;
967
- const window = options.report
968
- ? Math.min(OBSERVED_SWEEP_STAT_WINDOW, total)
969
- : total;
970
- let hashBytesLeft = OBSERVED_SWEEP_HASH_BUDGET_BYTES;
971
- const drifted = [];
972
- const unverifiable = [];
973
- let scanned = 0;
974
- let stoppedEarly = false;
975
- let steps = 0;
976
- /**
977
- * Re-baseline one file, and on the POST-DRAIN pass (`report: false`) retire its
978
- * `handled` mark at the same moment (#2449 review round 4, S2).
979
- *
980
- * Per FILE, not all-or-nothing. The mark exists to stop pi-lens's own drain
981
- * output being read as third-party drift, and it is safe to drop exactly when
982
- * the ledger has been moved onto the post-drain bytes for THAT file — which is
983
- * here. The previous cut cleared the whole set after the fact and only when
984
- * the pass completed, which was wrong in both directions: a parked pass kept
985
- * marks for files it HAD re-baselined (suppressing a real third-party change
986
- * to them until some later pass completed), and a completed pass dropped marks
987
- * for files it had skipped over.
988
- */
989
- const rebaseline = (ledgerKey, entry) => {
990
- putLedger(ledgerKey, entry);
991
- if (!options.report)
992
- current.handled.delete(ledgerKey);
993
- };
994
- for (; steps < window; steps += 1) {
995
- if (args.signal?.aborted === true) {
996
- stoppedEarly = true;
997
- break;
998
- }
999
- if (Date.now() >= options.deadlineMs) {
1000
- stoppedEarly = true;
1001
- break;
1002
- }
1003
- const filePath = tracked[(startCursor + steps) % total];
1004
- let key;
1005
- try {
1006
- key = normalizeMapKey(path.resolve(filePath));
1007
- }
1008
- catch {
1009
- continue;
1010
- }
1011
- let stat;
1012
- try {
1013
- stat = await fs.promises.stat(filePath);
1014
- }
1015
- catch {
1016
- // Gone, or unreadable. Deletions are not this net's business.
1017
- scanned += 1;
1018
- continue;
1019
- }
1020
- if (!stat.isFile()) {
1021
- scanned += 1;
1022
- continue;
1023
- }
1024
- scanned += 1;
1025
- const previous = current.ledger.get(key);
1026
- const next = {
1027
- size: stat.size,
1028
- mtimeMs: stat.mtimeMs,
1029
- hash: previous?.hash,
1030
- hashKind: previous?.hashKind,
1031
- seenAtMs: Date.now(),
1032
- };
1033
- if (previous === undefined) {
1034
- const dense = denseLineBaseline(args.getStoredLineHashes?.(key));
1035
- if (dense) {
1036
- next.hash = linesDigest(dense);
1037
- next.hashKind = "lines";
1038
- }
1039
- else if (stat.size <= hashBytesLeft) {
1040
- const content = await readBytesSafe(filePath);
1041
- if (content !== undefined) {
1042
- hashBytesLeft -= stat.size;
1043
- next.hash = createHash("sha256").update(content).digest("hex");
1044
- next.hashKind = "content";
1045
- }
1046
- }
1047
- rebaseline(key, next);
1048
- continue;
1049
- }
1050
- // The stat short-circuit — the reason this pass is affordable — but only
1051
- // when the baseline is old enough to be trusted. See `LedgerEntry.seenAtMs`
1052
- // for the same-tick same-size rewrite it would otherwise bake in forever.
1053
- //
1054
- // The reference instant is OBSERVED_LEDGER_SETTLE_MS *before* the entry
1055
- // was recorded: a file whose mtime is later than that was still in
1056
- // flight when we looked at it, so its stat cannot stand in for its
1057
- // bytes. `fresh` therefore means "already settled when recorded".
1058
- const baselineSettled = freshnessFromMtime({
1059
- mtimeMs: stat.mtimeMs,
1060
- referenceMs: previous.seenAtMs - OBSERVED_LEDGER_SETTLE_MS,
1061
- toleranceMs: 0,
1062
- }).verdict === "fresh";
1063
- if (previous.size === stat.size &&
1064
- previous.mtimeMs === stat.mtimeMs &&
1065
- baselineSettled) {
1066
- rebaseline(key, next);
1067
- continue;
1068
- }
1069
- // A drift CANDIDATE. Size moving is proof on its own; mtime moving is
1070
- // NOT (a `touch` bumps it without a byte moving), so mtime-only drift
1071
- // has to be confirmed against content before anything is replayed
1072
- // (#2449 round 2, F7).
1073
- const sizeChanged = previous.size !== stat.size;
1074
- let verdict = sizeChanged ? "drift" : "unverifiable";
1075
- if (stat.size <= hashBytesLeft) {
1076
- const content = await readBytesSafe(filePath);
1077
- if (content !== undefined) {
1078
- hashBytesLeft -= stat.size;
1079
- const contentHash = createHash("sha256").update(content).digest("hex");
1080
- if (previous.hash === undefined) {
1081
- verdict = sizeChanged ? "drift" : "unverifiable";
1082
- }
1083
- else if (previous.hashKind === "lines") {
1084
- verdict =
1085
- linesDigestOfContent(content.toString("utf-8")) !== previous.hash
1086
- ? "drift"
1087
- : "clean";
1088
- }
1089
- else {
1090
- verdict = contentHash !== previous.hash ? "drift" : "clean";
1091
- }
1092
- // Store the CONTENT hash either way, so the next change to this
1093
- // file is verifiable even if this one was not.
1094
- next.hash = contentHash;
1095
- next.hashKind = "content";
1096
- }
1097
- }
1098
- rebaseline(key, next);
1099
- if (!options.report)
1100
- continue;
1101
- if (current.handled.has(key))
1102
- continue;
1103
- if (args.isRecordable?.(key) === false)
1104
- continue;
1105
- if (verdict === "drift")
1106
- drifted.push(key);
1107
- else if (verdict === "unverifiable")
1108
- unverifiable.push(key);
1109
- }
1110
- if (options.report)
1111
- current.sweepCursor = (startCursor + steps) % total;
1112
- return {
1113
- scanned,
1114
- tracked: total,
1115
- cursor: options.report ? current.sweepCursor : 0,
1116
- drifted,
1117
- unverifiable,
1118
- stoppedEarly,
1119
- };
1120
- }
1121
- /**
1122
- * The turn-boundary net (#2430 item 3), run at `agent_settled` BEFORE the
1123
- * deferred drain so anything it finds is formatted in the same settle.
1124
- *
1125
- * Files the pipeline already recorded this run refresh their baseline and are
1126
- * never reported twice. Coverage is incremental and self-reported: see
1127
- * {@link scanTrackedIncrementally} and {@link SettledSweepResult}.
1128
- */
1129
- export async function runObservedSettledSweep(args) {
1130
- const started = Date.now();
1131
- const outcome = await withBounds(() => scanTrackedIncrementally(args, {
1132
- report: true,
1133
- deadlineMs: started + OBSERVED_CAPTURE_BUDGET_MS,
1134
- }),
1135
- // The inner loop parks its own cursor at the deadline, so this outer
1136
- // race exists only to bound a single wedged `stat` — hence the slack.
1137
- OBSERVED_CAPTURE_BUDGET_MS * 2, args.signal, { hook: "agent_settled", label: "runObservedSettledSweep" });
1138
- if (!outcome.ok) {
1139
- emitBounded("observed_sweep_skipped_budget", "settled-sweep", { durationMs: Date.now() - started, result: outcome.reason }, {
1140
- ledgerKind: "observed-mutation-budget",
1141
- reason: `settled sweep ${outcome.reason}`,
1142
- capPerTurn: { limit: 2, turnIndex: args.turnIndex },
1143
- });
1144
- return {
1145
- scanned: 0,
1146
- notReachedThisPass: 0,
1147
- cursor: state().sweepCursor,
1148
- drifted: [],
1149
- unverifiable: [],
1150
- replayed: 0,
1151
- reason: outcome.reason,
1152
- };
1153
- }
1154
- const scan = outcome.value;
1155
- const rangeBudget = {
1156
- remainingBytes: OBSERVED_SWEEP_RANGE_BUDGET_BYTES,
1157
- };
1158
- let replayed = 0;
1159
- for (const filePath of scan.drifted) {
1160
- if (args.signal?.aborted === true)
1161
- break;
1162
- const editRanges = await deriveObservedEditRanges(filePath, args.getStoredLineHashes?.(filePath), rangeBudget);
1163
- const accepted = args.record({
1164
- filePath,
1165
- kind: "edit",
1166
- touchedLines: editRanges ? boundingBox(editRanges) : undefined,
1167
- editRanges: editRanges && editRanges.length > 1 ? editRanges : undefined,
1168
- consumer: "settled-sweep",
1169
- provenance: "settled-sweep",
1170
- });
1171
- if (accepted)
1172
- replayed += 1;
1173
- }
1174
- const notReachedThisPass = Math.max(0, scan.tracked - scan.scanned);
1175
- if (scan.drifted.length > 0 || scan.unverifiable.length > 0) {
1176
- logLatency({
1177
- type: "phase",
1178
- phase: "observed_settled_sweep_drift",
1179
- filePath: scan.drifted.slice(0, 5).join(","),
1180
- durationMs: Date.now() - started,
1181
- result: `drifted:${scan.drifted.length} replayed:${replayed} unverifiable:${scan.unverifiable.length} scanned:${scan.scanned}/${scan.tracked} cursor:${scan.cursor}`,
1182
- });
1183
- }
1184
- if (scan.unverifiable.length > 0) {
1185
- // Named, capped, and never replayed: a stat that moved without a
1186
- // provable content change is a gap in coverage, and a gap that reports
1187
- // itself is the only kind that gets fixed (catalog shape 10).
1188
- emitBounded("observed_sweep_unverifiable", "settled-sweep", {
1189
- durationMs: Date.now() - started,
1190
- result: `unverifiable:${scan.unverifiable.length}`,
1191
- filePath: scan.unverifiable.slice(0, 5).join(","),
1192
- }, {
1193
- ledgerKind: "observed-mutation-budget",
1194
- reason: "tracked file's size/mtime moved but no hashed baseline could confirm a content change",
1195
- capPerTurn: { limit: 2, turnIndex: args.turnIndex },
1196
- });
1197
- }
1198
- return {
1199
- scanned: scan.scanned,
1200
- notReachedThisPass,
1201
- cursor: scan.cursor,
1202
- drifted: scan.drifted,
1203
- unverifiable: scan.unverifiable,
1204
- replayed,
1205
- reason: scan.stoppedEarly ? "window-parked" : undefined,
1206
- };
1207
- }
1208
- /**
1209
- * Re-baseline pi-lens's OWN writes AFTER the deferred drain.
1210
- *
1211
- * The drain is pi-lens formatting and autofixing files it already knows about,
1212
- * so those bytes are ours. Without this the next settle would read them as
1213
- * third-party drift and requeue the same files forever.
1214
- *
1215
- * ## The traversal is `handled`, not the tracked set (#2449 review round 5, F2)
1216
- *
1217
- * Every file this function needs to re-baseline is, by construction, already
1218
- * in `handled`: that set exists exactly to name "pi-lens wrote these bytes
1219
- * this run" (see `noteMutationHandled`), and this function's only job is to
1220
- * clear that claim once the ledger agrees. Walking `getTrackedPaths()`
1221
- * instead — the earlier shape — coupled this function's cost and completion
1222
- * to the size of the TRACKED set (up to `OBSERVED_TRACKED_MAX_FILES`, i.e.
1223
- * 400) rather than to the size of the DRAIN (a handful of files), and on a
1224
- * tracked set too large to finish inside `OBSERVED_CAPTURE_BUDGET_MS` the
1225
- * pass parked at the same prefix every turn — `report: false` always starts
1226
- * its cursor at 0 — so files in the tail never got re-baselined and their
1227
- * `handled` marks never retired, permanently suppressing drift reports for
1228
- * them (reviewer PROBE-B1).
1229
- *
1230
- * `handled` is bounded by `OBSERVED_HANDLED_MAX` and holds only files the
1231
- * pipeline or the drain actually wrote this run, so iterating it is
1232
- * O(handful) rather than O(tracked set) and — barring an aborted turn or a
1233
- * genuinely pathological handled-set size — completes in one pass every time.
1234
- * That is what lets every mark retire on every refresh instead of only the
1235
- * marks a rotating cursor happened to reach.
1236
- *
1237
- * ## The retirement is still per FILE (#2449 review rounds 3 (S5) and 4 (S2))
1238
- *
1239
- * A mark is dropped exactly when the ledger has been moved onto the
1240
- * POST-drain bytes for that same file — inside the scan, one file at a time
1241
- * (see `rebaseline`) — rather than all-or-nothing on completion. An aborted
1242
- * or truncated pass therefore still retires the marks for the files it DID
1243
- * reach and correctly leaves the rest standing; it no longer needs a
1244
- * dedicated "coverage gap" record to stay honest, because the traversal it
1245
- * covers is the whole of what it was asked to do.
1246
- */
1247
- export async function refreshObservedMutationLedger(args) {
1248
- const outcome = await withBounds(() => scanTrackedIncrementally(args, {
1249
- report: false,
1250
- deadlineMs: Date.now() + OBSERVED_CAPTURE_BUDGET_MS,
1251
- }), OBSERVED_CAPTURE_BUDGET_MS * 2, args.signal, { hook: "agent_settled", label: "refreshObservedMutationLedger" });
1252
- return outcome.ok ? outcome.value.scanned : 0;
1253
- }