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
@@ -79,6 +79,7 @@ async function main() {
79
79
  analyzeActionableWarnings(files.actionableWarnings, state),
80
80
  analyzeAstGrepTools(files.astGrepTools, state),
81
81
  analyzeWorklog(files.worklog, state),
82
+ analyzeExtension(files.extension, state),
82
83
  ]);
83
84
 
84
85
  const report = buildReport(state);
@@ -165,18 +166,34 @@ function discoverLogFiles(logRoot, archived) {
165
166
  });
166
167
 
167
168
  return {
168
- latency: byPrefix("latency"),
169
- sessionStart: byPrefix("sessionstart"),
170
- cascade: byPrefix("cascade"),
171
- readGuard: byPrefix("read-guard"),
172
- treeSitter: byPrefix("tree-sitter"),
173
- actionableWarnings: byPrefix("actionable-warnings"),
174
- astGrepTools: byPrefix("ast-grep-tools"),
169
+ latency: chronologicalFiles(byPrefix("latency")),
170
+ sessionStart: chronologicalFiles(byPrefix("sessionstart")),
171
+ cascade: chronologicalFiles(byPrefix("cascade")),
172
+ readGuard: chronologicalFiles(byPrefix("read-guard")),
173
+ treeSitter: chronologicalFiles(byPrefix("tree-sitter")),
174
+ actionableWarnings: chronologicalFiles(byPrefix("actionable-warnings")),
175
+ astGrepTools: chronologicalFiles(byPrefix("ast-grep-tools")),
176
+ extension: chronologicalFiles(byPrefix("extension")),
175
177
  diagnostics: dailyLogs,
176
178
  worklog: worklogs,
177
179
  };
178
180
  }
179
181
 
182
+ /**
183
+ * Rotated siblings (`x.log.1`) carry older rows than the active `x.log`. The
184
+ * run-splitting detectors (D3/D4/D5) and the read-guard evidence walk (D10)
185
+ * only read correctly in chronological order, so order every multi-file stream
186
+ * oldest-first with the active file last.
187
+ */
188
+ function chronologicalFiles(files) {
189
+ return [...files].sort((a, b) => {
190
+ const activeA = path.basename(a).endsWith(".log");
191
+ const activeB = path.basename(b).endsWith(".log");
192
+ if (activeA !== activeB) return activeA ? 1 : -1;
193
+ return a.localeCompare(b);
194
+ });
195
+ }
196
+
180
197
  function safeReaddir(dir) {
181
198
  try {
182
199
  return fs.readdirSync(dir);
@@ -205,6 +222,45 @@ function createState(files) {
205
222
  toolResults: counter(),
206
223
  phaseCounts: counter(),
207
224
  phaseTimeouts: counter(),
225
+ // D1: every latency row timestamp (ms) in window, for the coverage-gap
226
+ // scan. Sorted after all files are read.
227
+ rowTs: [],
228
+ cascadeTs: [],
229
+ // D2: real-log pollution — rows whose filePath points at a test
230
+ // home, grouped by pid.
231
+ scratchRows: new Map(),
232
+ // D5: test_runner_delivery outcomes per session, joined with the
233
+ // sessionstart firing/stale text into one delivery-health verdict.
234
+ testRunnerDelivery: new Map(),
235
+ // D6: `phase: knip` executed rows per pid, for the drift and cost rules.
236
+ knip: new Map(),
237
+ // D7: hook-await-exceeded overruns plus the per-pid degradation census.
238
+ hookAwaitExceeded: [],
239
+ degradationKinds: new Map(),
240
+ // D8: turn_end `tool_result` summaries per pid, and the retained-state text.
241
+ turnEndTools: new Map(),
242
+ retainingNewerTurn: [],
243
+ // D9: empty-candidate LSP waits, edit touches that saw no client, and
244
+ // warm-reuse selections, joined into the wait/contradiction verdict.
245
+ lspWaitEmpty: new Map(),
246
+ lspTouchEdit: new Map(),
247
+ lspTouchNoClients: [],
248
+ lspClientSelectedWarm: [],
249
+ // D11: read-set carry outcomes at a restart boundary.
250
+ carryRestart: [],
251
+ // D12: nudges and scope transitions, joined by pid + wall clock.
252
+ agentNudges: [],
253
+ sessionScopeTransitions: [],
254
+ // D13: deferred runner failures joined to their delivery row by (pid, turnId).
255
+ deferredRunnerFailed: [],
256
+ lateRunnerFindings: [],
257
+ // D14: auxiliary stuck pair (pid, filePath, serverId) recurrence.
258
+ auxStuck: new Map(),
259
+ // D15: advisory provenance decisions with unknown provenance.
260
+ advisoryProvenance: [],
261
+ // D8/D15: first latency row timestamp per pid (lifetime base).
262
+ pidFirstTs: new Map(),
263
+ pidLastTs: new Map(),
208
264
  workspace: {
209
265
  started: 0,
210
266
  completed: 0,
@@ -280,6 +336,24 @@ function createState(files) {
280
336
  staleRanges: [],
281
337
  zeroReads: [],
282
338
  unavailableSnapshots: [],
339
+ // E2: the block/warn split. These rows do not carry a host-side
340
+ // decision, so the analyzer must not invent one.
341
+ blocks: [],
342
+ warns: [],
343
+ blockByKind: counter(),
344
+ bypassedMismatch: 0,
345
+ // D10: per-(session,file) evidence that a read or a committed edit
346
+ // happened earlier in the session, so a later `zero_read` block is a
347
+ // lost read-set rather than a genuine cold edit.
348
+ fileEvidence: new Map(),
349
+ stateLost: [],
350
+ genuineZeroRead: [],
351
+ },
352
+ extension: {
353
+ // D2: extension.log rows whose payload names a test home
354
+ // directory, grouped by pid, plus the warn/error census.
355
+ scratchRows: new Map(),
356
+ warnErrorGroups: counter(),
283
357
  },
284
358
  treeSitter: {
285
359
  phases: counter(),
@@ -292,12 +366,26 @@ function createState(files) {
292
366
  session: {
293
367
  starts: 0,
294
368
  cwds: counter(),
369
+ // E4: per-start build identity (`commit=`), so every rate can be split
370
+ // by build. Keyed by commit, with the start timestamps that carried it.
371
+ commits: counter(),
295
372
  slowStarts: [],
296
373
  slowTasks: [],
297
374
  toolNoise: counter(),
298
375
  lspNoise: counter(),
299
376
  errors: [],
300
- rotations: counter(),
377
+ // D1: every sessionstart line timestamp (ms) in window.
378
+ rowTs: [],
379
+ // D16: `pi-lens loaded: Nms` lines, with their wall clock so a short
380
+ // lived pid can be marked.
381
+ slowLoads: [],
382
+ // D16: the wall clock of every `session_start fired`, so a load with no
383
+ // session start within 60 s (a short-lived pid) is visible.
384
+ firedTs: [],
385
+ // D3/D4/D5/D8: one run per `session_start fired`. Run splitting is the
386
+ // only way to attribute a turn-end test decision to the session that made it.
387
+ runs: [],
388
+ currentRun: newRun(),
301
389
  },
302
390
  actionable: {
303
391
  events: counter(),
@@ -329,13 +417,26 @@ function createState(files) {
329
417
  };
330
418
  }
331
419
 
420
+ /**
421
+ * The failure kind of a `failed` or `crashed` runner row. `metadata.failureKind`
422
+ * is exact: inline rows carry it (#3781) and collect-later rows too (#3796).
423
+ * Rows written by released versions never carry it, so the count heuristic
424
+ * (a failure with diagnostics is found-errors) stays only as that fallback.
425
+ */
426
+ function runnerFailureKind(entry) {
427
+ if (entry.metadata?.failureKind) return entry.metadata.failureKind;
428
+ if (entry.status === "crashed") return "crashed";
429
+ return (entry.diagnosticCount ?? 0) > 0 ? "blocking_diagnostics" : "unknown";
430
+ }
431
+
332
432
  async function analyzeLatency(files, state) {
333
433
  for (const file of files) {
334
434
  await forEachJsonLine(file, "latency", state, (entry) => {
335
435
  const ts = dateOf(entry.ts);
336
436
  if (!inWindow(ts)) return;
337
437
  state.seen.inc("latency");
338
- trackProject(state, entry.filePath);
438
+ trackLatencyProject(state, entry);
439
+ trackLatencySignals(state, entry, ts);
339
440
 
340
441
  if (entry.type === "runner") {
341
442
  const status = entry.status ?? "unknown";
@@ -344,15 +445,8 @@ async function analyzeLatency(files, state) {
344
445
  if (status === "failed" || status === "crashed") {
345
446
  // Separate a genuine runner breakage from "the check ran and found
346
447
  // blocking issues" (e.g. the LSP runner reports status:failed when a
347
- // file has type errors). Prefer the logged failureKind; fall back to
348
- // the heuristic that a failure carrying diagnostics is found-errors.
349
- const kind =
350
- entry.metadata?.failureKind ??
351
- (status === "crashed"
352
- ? "crashed"
353
- : (entry.diagnosticCount ?? 0) > 0
354
- ? "blocking_diagnostics"
355
- : "unknown");
448
+ // file has type errors).
449
+ const kind = runnerFailureKind(entry);
356
450
  state.latency.runnerFailureKinds.inc(`${runner}:${kind}`);
357
451
  if (kind === "blocking_diagnostics") {
358
452
  state.latency.runnerBlockingFindings.inc(runner);
@@ -543,6 +637,7 @@ async function analyzeCascade(files, state) {
543
637
  const ts = dateOf(entry.ts);
544
638
  if (!inWindow(ts)) return;
545
639
  state.seen.inc("cascade");
640
+ if (ts) state.latency.cascadeTs.push(ts.getTime());
546
641
  trackProject(state, entry.filePath);
547
642
  const phase = entry.phase ?? "unknown";
548
643
  state.cascade.phases.inc(phase);
@@ -589,24 +684,76 @@ async function analyzeReadGuard(files, state) {
589
684
  for (const file of files) {
590
685
  await forEachJsonLine(file, "read-guard", state, (entry) => {
591
686
  const ts = dateOf(entry.ts);
592
- if (!inWindow(ts)) return;
687
+ if (!inWindow(ts)) {
688
+ // D10 needs evidence that may precede a live-monitor window.
689
+ // Keep this lookback state private; outside rows never affect counts.
690
+ if (hasReadEvidence(entry)) {
691
+ const key = `${readGuardSessionOf(entry) ?? "?"}\u0000${entry.filePath ?? "?"}`;
692
+ state.readGuard.fileEvidence.set(key, true);
693
+ }
694
+ return;
695
+ }
593
696
  state.seen.inc("read-guard");
594
697
  trackProject(state, entry.filePath);
595
698
  const event = entry.event ?? "unknown";
699
+ const md = entry.metadata ?? {};
596
700
  state.readGuard.events.inc(event);
597
701
  state.readGuard.byFile.inc(shortPath(entry.filePath));
598
- const reasonKind = entry.metadata?.reasonKind;
702
+ const reasonKind = md.reasonKind;
599
703
  if (reasonKind) state.readGuard.byReason.inc(reasonKind);
704
+
705
+ // D10: evidence that this file was read or edited earlier in the
706
+ // SAME session. Read before this row's own evidence is recorded, so a
707
+ // later `zero_read` block sees only the prior rows. An
708
+ // `edit_batch_summary` carries no sessionId, so its session is the
709
+ // turnId prefix before the last `:`.
710
+ const evidenceKey = `${readGuardSessionOf(entry) ?? "?"}\u0000${entry.filePath ?? "?"}`;
711
+ const hadEvidence =
712
+ state.readGuard.fileEvidence.get(evidenceKey) === true;
713
+
600
714
  if (event === "edit_preflight_blocked") {
601
715
  state.readGuard.preflightReasons.inc(reasonKind ?? "unknown");
716
+ registerBlock(state, entry, "preflight");
717
+ }
718
+ if (event === "edit_blocked") {
719
+ const summary = registerBlock(state, entry, "blocked");
720
+ if (reasonKind === "zero_read") {
721
+ pushTop(state.readGuard.zeroReads, summary, limit * 3, byLine);
722
+ if (hadEvidence) {
723
+ pushTop(
724
+ state.readGuard.stateLost,
725
+ { ...summary, classification: "state-lost" },
726
+ limit * 3,
727
+ byLine,
728
+ );
729
+ } else {
730
+ pushTop(
731
+ state.readGuard.genuineZeroRead,
732
+ { ...summary, classification: "genuine" },
733
+ limit * 3,
734
+ byLine,
735
+ );
736
+ }
737
+ }
738
+ }
739
+ if (event === "edit_warned") {
740
+ pushTop(
741
+ state.readGuard.warns,
742
+ summarizeReadGuard(entry),
743
+ limit * 3,
744
+ byLine,
745
+ );
602
746
  }
603
747
  if (event === "range_snapshot_validation") {
604
- const status = entry.metadata?.status ?? "unknown";
748
+ const status = md.status ?? "unknown";
749
+ const outcome = String(md.outcome ?? "");
605
750
  state.readGuard.snapshotStatus.inc(status);
606
751
  state.readGuard.snapshotEnforcement.inc(
607
- entry.metadata?.enforced ? "enforced" : "not_enforced",
752
+ md.enforced ? "enforced" : "not_enforced",
608
753
  );
609
- if (status === "mismatch") {
754
+ // E3: `bypassed-content-match` is an allowed edit, not a stale
755
+ // read. Only a mismatch the caller did NOT bypass is a smell.
756
+ if (status === "mismatch" && !outcome.startsWith("bypassed")) {
610
757
  state.smellTotals.inc("read-guard-stale-ranges");
611
758
  pushTop(
612
759
  state.readGuard.staleRanges,
@@ -614,6 +761,8 @@ async function analyzeReadGuard(files, state) {
614
761
  limit * 3,
615
762
  byLine,
616
763
  );
764
+ } else if (status === "mismatch") {
765
+ state.readGuard.bypassedMismatch += 1;
617
766
  } else if (status === "unavailable") {
618
767
  pushTop(
619
768
  state.readGuard.unavailableSnapshots,
@@ -623,21 +772,11 @@ async function analyzeReadGuard(files, state) {
623
772
  );
624
773
  }
625
774
  }
626
- if (event === "edit_blocked" || event === "edit_warned") {
627
- state.smellTotals.inc("read-guard-friction");
628
- const summary = summarizeReadGuard(entry);
629
- pushTop(state.readGuard.blocked, summary, limit * 3, byLine);
630
- if (reasonKind === "zero_read") {
631
- pushTop(state.readGuard.zeroReads, summary, limit * 3, byLine);
632
- }
633
- }
634
775
  if (
635
776
  event === "oldtext_not_found" ||
636
777
  event === "oldtext_duplicate" ||
637
- event === "touched_lines_missing" ||
638
- event === "edit_preflight_blocked"
778
+ event === "touched_lines_missing"
639
779
  ) {
640
- state.smellTotals.inc("read-guard-friction");
641
780
  pushTop(
642
781
  state.readGuard.oldTextIssues,
643
782
  summarizeReadGuard(entry),
@@ -645,10 +784,48 @@ async function analyzeReadGuard(files, state) {
645
784
  byLine,
646
785
  );
647
786
  }
787
+
788
+ if (hasReadEvidence(entry))
789
+ state.readGuard.fileEvidence.set(evidenceKey, true);
648
790
  });
649
791
  }
650
792
  }
651
793
 
794
+ /** D10: a row proving the file was read or edited earlier in its session. */
795
+ function hasReadEvidence(entry) {
796
+ const md = entry.metadata ?? {};
797
+ return (
798
+ entry.event === "edit_batch_summary" ||
799
+ (entry.event === "range_snapshot_validation" &&
800
+ Number(md.candidateReadCount ?? 0) > 0) ||
801
+ (entry.event === "edit_warned" && Number(md.readCount ?? 0) > 0)
802
+ );
803
+ }
804
+
805
+ /**
806
+ * D10: the session a read-guard row belongs to. `edit_batch_summary` rows carry
807
+ * no `sessionId`, so fall back to the turnId prefix before the last `:` (the
808
+ * report's own join instruction).
809
+ */
810
+ function readGuardSessionOf(entry) {
811
+ if (typeof entry.sessionId === "string" && entry.sessionId)
812
+ return entry.sessionId;
813
+ const turnId = entry.turnId;
814
+ if (typeof turnId === "string" && turnId.includes(":"))
815
+ return turnId.slice(0, turnId.lastIndexOf(":"));
816
+ return null;
817
+ }
818
+
819
+ /** E2: one blocked edit. Host/model provenance is not present on this row. */
820
+ function registerBlock(state, entry, source) {
821
+ const summary = summarizeReadGuard(entry);
822
+ pushTop(state.readGuard.blocks, summary, limit * 3, byLine);
823
+ state.readGuard.blockByKind.inc(
824
+ `${source}:${entry.metadata?.reasonKind ?? "unknown"}`,
825
+ );
826
+ return summary;
827
+ }
828
+
652
829
  async function analyzeTreeSitter(files, state) {
653
830
  for (const file of files) {
654
831
  await forEachJsonLine(file, "tree-sitter", state, (entry) => {
@@ -704,21 +881,48 @@ async function analyzeSessionStart(files, state) {
704
881
  const match = lineRe.exec(line);
705
882
  if (!match) return;
706
883
  const ts = dateOf(match[1]);
707
- if (!inWindow(ts)) return;
708
884
  const message = match[2];
885
+ if (!inWindow(ts)) {
886
+ // A --since window can begin after the session_start that owns its
887
+ // rows. Carry only that run anchor into the visible window.
888
+ if (message.startsWith("session_start fired")) {
889
+ state.session.currentRun = newRun();
890
+ state.session.currentRun.startTs = iso(ts);
891
+ }
892
+ return;
893
+ }
709
894
  if (isExcludedText(message)) {
710
895
  state.excludedRows++;
711
896
  return;
712
897
  }
713
898
  state.seen.inc("sessionstart");
899
+ if (ts) state.session.rowTs.push(ts.getTime());
900
+
901
+ // E4: starts are counted from `session_start fired`, which every mode
902
+ // writes, instead of the dead `session_start cwd:` full-mode line. The
903
+ // same line opens a D3/D4/D5/D8 run.
904
+ if (message.startsWith("session_start fired")) {
905
+ state.session.starts++;
906
+ if (ts) state.session.firedTs.push(ts.getTime());
907
+ state.session.runs.push(state.session.currentRun);
908
+ state.session.currentRun = newRun();
909
+ state.session.currentRun.startTs = iso(ts);
910
+ }
714
911
 
715
912
  const cwd = /session_start cwd:\s*(.*)$/.exec(message)?.[1];
716
913
  if (cwd) {
717
- state.session.starts++;
718
914
  state.session.cwds.inc(projectOf(cwd));
719
915
  trackProject(state, cwd);
720
916
  }
721
917
 
918
+ // E4: build identity for the session that just started.
919
+ const buildIdentity =
920
+ /session_start: build identity [—-] commit=(\w+)/.exec(message);
921
+ if (buildIdentity) {
922
+ state.session.commits.inc(buildIdentity[1]);
923
+ state.session.currentRun.commit = buildIdentity[1];
924
+ }
925
+
722
926
  // #2526 R3 S1: this session's config-resolution PENDING mark, published
723
927
  // by `loadLSPConfig` itself at the instant a resolution is actually
724
928
  // attempted — never a start-line prediction from `no-lsp`/subagent/
@@ -780,8 +984,49 @@ async function analyzeSessionStart(files, state) {
780
984
  );
781
985
  }
782
986
 
783
- if (message.includes("log_cleanup: rotated"))
784
- state.session.rotations.inc(message.replace(/^log_cleanup:\s*/, ""));
987
+ // D16: extension load time is invisible to `slow-session-start`, which
988
+ // reads `session_start total:` (a quick-mode total of tens of ms).
989
+ const load = /pi-lens loaded: (\d+)ms after process start/.exec(message);
990
+ if (load) {
991
+ state.session.slowLoads.push({
992
+ ts: iso(ts),
993
+ ms: ts ? ts.getTime() : null,
994
+ durationMs: Number(load[1]),
995
+ message,
996
+ });
997
+ }
998
+
999
+ const run = state.session.currentRun;
1000
+ const modified = /turn_end: (\d+) file\(s\) modified/.exec(message);
1001
+ if (modified) run.edits += 1;
1002
+ if (message.includes("excluded by the built-in turn-end policy")) {
1003
+ run.excluded += 1;
1004
+ const excludedPath = /turn_end:\s*(\S+)\s+→ test target excluded/.exec(
1005
+ message,
1006
+ )?.[1];
1007
+ if (excludedPath) run.excludedTestFiles.push(excludedPath);
1008
+ }
1009
+ if (message.includes("→ no test file found")) run.noTestFile += 1;
1010
+ const vitest =
1011
+ /^turn_end:\s+(.+?)\s+→\s+test vitest\s+(\S+)\s+\(([^)]+)\)/.exec(
1012
+ message,
1013
+ );
1014
+ if (vitest) {
1015
+ run.ran += 1;
1016
+ const [, src, tgt, mode] = vitest;
1017
+ if (mode.includes("failed-first")) {
1018
+ run.failedFirst += 1;
1019
+ const a = checkoutOf(src);
1020
+ const b = checkoutOf(tgt);
1021
+ if (a && b && a !== b) run.crossCheckout += 1;
1022
+ }
1023
+ }
1024
+ if (/turn_end: firing \d+ test target\(s\) async/.test(message))
1025
+ run.testFirings += 1;
1026
+ if (/\(stale\s*[—-]\s*turn advanced while tests ran\)/.test(message))
1027
+ run.testStale += 1;
1028
+ if (/turn_end: retaining newer turn state/.test(message))
1029
+ run.retainingNewerTurn += 1;
785
1030
 
786
1031
  const lower = message.toLowerCase();
787
1032
  if (
@@ -792,8 +1037,16 @@ async function analyzeSessionStart(files, state) {
792
1037
  state.smellTotals.inc("tool-install-noise");
793
1038
  state.session.toolNoise.inc(normalizeSessionNoise(message));
794
1039
  }
1040
+ // E1: anchor to the production failure emitters so a worktree name
1041
+ // such as `468-wait-timeout` inside a `cwd=`/`command=` token cannot
1042
+ // match (the 20 false positives in the live session). Every emitter
1043
+ // puts the failure word before any `key=` token: `lsp spawn <id>:
1044
+ // unavailable|failed` (clients/lsp/index.ts) and `lsp launch
1045
+ // candidate|managed|bundle|tree-bin failed` (clients/lsp/server.ts).
1046
+ // `lsp read warm unavailable` is informational, and `lsp process
1047
+ // <cmd>:` only ever logs `spawn-error` or `closed` (clients/lsp/launch.ts).
795
1048
  if (
796
- /lsp .*?(unavailable|failed|timeout|skipped_broken|exited immediately|binary not found)/i.test(
1049
+ /^lsp (?:spawn [^:]+: (?:unavailable|failed)|launch (?:candidate|managed|bundle|tree-bin) failed)/.test(
797
1050
  message,
798
1051
  )
799
1052
  ) {
@@ -814,6 +1067,9 @@ async function analyzeSessionStart(files, state) {
814
1067
  }
815
1068
  });
816
1069
  }
1070
+ // Close the final run so the last `session_start fired` block is analyzed.
1071
+ if (state.session.currentRun.startTs)
1072
+ state.session.runs.push(state.session.currentRun);
817
1073
  }
818
1074
 
819
1075
  async function analyzeActionableWarnings(files, state) {
@@ -984,12 +1240,18 @@ function pathValues(value, key = "") {
984
1240
  }
985
1241
 
986
1242
  function isExcludedText(text) {
1243
+ // E4: the old match only ever saw the dead `session_start cwd:` line. The
1244
+ // live cwd/root carriers are the pending-resolution mark and the spawn lines.
1245
+ const values = [];
987
1246
  const cwd = /session_start cwd:\s*(.*)$/.exec(text)?.[1];
988
- return cwd
989
- ? excludeGlobs.some((glob) =>
990
- minimatch(normalizePath(cwd), glob, { nocase: true, dot: true }),
991
- )
992
- : false;
1247
+ if (cwd) values.push(cwd);
1248
+ for (const match of text.matchAll(/\b(?:cwd|root)=(\S+)/g))
1249
+ values.push(match[1]);
1250
+ return values.some((value) =>
1251
+ excludeGlobs.some((glob) =>
1252
+ minimatch(normalizePath(value), glob, { nocase: true, dot: true }),
1253
+ ),
1254
+ );
993
1255
  }
994
1256
 
995
1257
  async function forEachLine(file, visitor) {
@@ -1200,19 +1462,18 @@ function summarizeCascade(entry) {
1200
1462
  }
1201
1463
 
1202
1464
  function summarizeReadGuard(entry) {
1465
+ const md = entry.metadata ?? {};
1466
+ // R2: the old offset/limit/symbol fields were undefined on 100% of rows.
1467
+ // `touchedLines[0]`/`range[0]` are the real line identity the rows carry.
1468
+ const touched = Array.isArray(md.touchedLines) ? md.touchedLines : [];
1469
+ const range = Array.isArray(md.range) ? md.range : [];
1203
1470
  return {
1204
1471
  ts: entry.ts,
1205
1472
  event: entry.event,
1206
1473
  filePath: shortPath(entry.filePath),
1207
1474
  project: projectOf(entry.filePath),
1208
- requestedOffset: entry.requestedOffset,
1209
- requestedLimit: entry.requestedLimit,
1210
- effectiveOffset: entry.effectiveOffset,
1211
- effectiveLimit: entry.effectiveLimit,
1212
- symbol: entry.symbol,
1213
- symbolKind: entry.symbolKind,
1214
- line: entry.symbolStartLine,
1215
- metadata: entry.metadata,
1475
+ line: touched[0] ?? range[0],
1476
+ metadata: md,
1216
1477
  };
1217
1478
  }
1218
1479
 
@@ -1295,9 +1556,613 @@ function normalizePath(filePath) {
1295
1556
  return String(filePath).replace(/\\/g, "/");
1296
1557
  }
1297
1558
 
1559
+ /** One D3/D4/D5/D8 run, opened by a `session_start fired` line. */
1560
+ function newRun() {
1561
+ return {
1562
+ startTs: null,
1563
+ commit: null,
1564
+ edits: 0,
1565
+ ran: 0,
1566
+ failedFirst: 0,
1567
+ crossCheckout: 0,
1568
+ excluded: 0,
1569
+ excludedTestFiles: [],
1570
+ noTestFile: 0,
1571
+ retainingNewerTurn: 0,
1572
+ testFirings: 0,
1573
+ testStale: 0,
1574
+ };
1575
+ }
1576
+
1577
+ /** The checkout a turn-end path belongs to (`.worktrees/<id>/` or the main `src/`). */
1578
+ function checkoutOf(p) {
1579
+ const m = /\.worktrees\/([^/]+)\//.exec(p);
1580
+ if (m) return m[1];
1581
+ if (/^(?:src|tests?)\//.test(p)) return "main";
1582
+ return null;
1583
+ }
1584
+
1585
+ /**
1586
+ * D2: the markers only a test home carries: the #3521 fork-tree witness home
1587
+ * and the suite's `pi-lens-test-*` temp dirs. `pi-lens-worktrees`,
1588
+ * `pi-lens-orchestrator/tmp` and `.probe-home/` are not markers: real
1589
+ * sessions edit there (the orchestrator writes its task files under
1590
+ * `.probe-home/orchestration/`).
1591
+ */
1592
+ const SCRATCH_PATH_RE = /(witness-home|pi-lens-test-)/;
1593
+
1594
+ /**
1595
+ * E5/D2: the `opaque_mutation_*` phases log the bash command as `filePath`
1596
+ * (clients/runtime-tool-call.ts, clients/runtime-tool-result.ts), except
1597
+ * `opaque_mutation_recovered`, which logs the recovered paths.
1598
+ */
1599
+ function isCommandTextRow(entry) {
1600
+ const phase = String(entry.phase ?? "");
1601
+ return (
1602
+ phase.startsWith("opaque_mutation_") &&
1603
+ phase !== "opaque_mutation_recovered"
1604
+ );
1605
+ }
1606
+
1607
+ /** E5: track a latency row's project unless its `filePath` is a shell command. */
1608
+ function trackLatencyProject(state, entry) {
1609
+ const filePath = entry?.filePath;
1610
+ if (typeof filePath !== "string" || filePath.length === 0) return;
1611
+ if (isCommandTextRow(entry)) return;
1612
+ // The `<pi-lens>` sentinel is never a project path.
1613
+ if (filePath.startsWith("<")) return;
1614
+ state.projects.inc(projectOf(filePath));
1615
+ }
1616
+
1617
+ function trackLatencyPollution(state, entry) {
1618
+ if (isCommandTextRow(entry)) return;
1619
+ const hit = pathValues(entry).some((value) => SCRATCH_PATH_RE.test(value));
1620
+ if (!hit) return;
1621
+ const pid = String(entry.pid ?? "unknown");
1622
+ state.latency.scratchRows.set(
1623
+ pid,
1624
+ (state.latency.scratchRows.get(pid) ?? 0) + 1,
1625
+ );
1626
+ }
1627
+
1628
+ /**
1629
+ * Every latency signal the D5/D6/D7/D9/D11-D15 detectors read. Kept in one
1630
+ * function so the streaming visitor stays a thin fan-out.
1631
+ */
1632
+ function trackLatencySignals(state, entry, ts) {
1633
+ const ms = ts instanceof Date ? ts.getTime() : null;
1634
+ if (ms != null && Number.isFinite(ms)) state.latency.rowTs.push(ms);
1635
+ const pid = String(entry.pid ?? "unknown");
1636
+ if (ms != null && Number.isFinite(ms)) {
1637
+ const first = state.latency.pidFirstTs.get(pid);
1638
+ if (first == null || ms < first) state.latency.pidFirstTs.set(pid, ms);
1639
+ const last = state.latency.pidLastTs.get(pid);
1640
+ if (last == null || ms > last) state.latency.pidLastTs.set(pid, ms);
1641
+ }
1642
+ trackLatencyPollution(state, entry);
1643
+
1644
+ if (entry.type === "runner") {
1645
+ const md = entry.metadata ?? {};
1646
+ if (
1647
+ entry.status === "failed" &&
1648
+ md.tier === "collect-later" &&
1649
+ Number(entry.diagnosticCount ?? 0) > 0 &&
1650
+ runnerFailureKind(entry) === "blocking_diagnostics"
1651
+ ) {
1652
+ state.latency.deferredRunnerFailed.push({
1653
+ ts: entry.ts,
1654
+ ms,
1655
+ pid,
1656
+ turnId: entry.turnId,
1657
+ runnerId: entry.runnerId,
1658
+ diagnosticCount: Number(entry.diagnosticCount ?? 0),
1659
+ filePath: entry.filePath,
1660
+ });
1661
+ }
1662
+ return;
1663
+ }
1664
+
1665
+ if (entry.type === "tool_result") {
1666
+ // D8: the turn_end summary row carries `blockerSections`; the existing
1667
+ // tool_result branch only counts `result`, so it never saw this duration.
1668
+ if (entry.metadata && entry.metadata.blockerSections != null) {
1669
+ const rows = state.latency.turnEndTools.get(pid) ?? [];
1670
+ rows.push({
1671
+ ts: entry.ts,
1672
+ ms,
1673
+ durationMs: Number(entry.durationMs ?? 0),
1674
+ });
1675
+ state.latency.turnEndTools.set(pid, rows);
1676
+ }
1677
+ return;
1678
+ }
1679
+ if (entry.type !== "phase") return;
1680
+
1681
+ const phase = entry.phase ?? "unknown";
1682
+ const md = entry.metadata ?? {};
1683
+ const nowIso = entry.ts;
1684
+
1685
+ if (phase === "test_runner_delivery") {
1686
+ const sid = md.sessionId ?? pid;
1687
+ const c = state.latency.testRunnerDelivery.get(sid) ?? counter();
1688
+ c.inc(md.outcome ?? "unknown");
1689
+ state.latency.testRunnerDelivery.set(sid, c);
1690
+ } else if (phase === "knip") {
1691
+ if (
1692
+ md.execution === "executed" &&
1693
+ Number.isFinite(Number(md.totalIssues))
1694
+ ) {
1695
+ const rows = state.latency.knip.get(pid) ?? [];
1696
+ rows.push({
1697
+ ts: nowIso,
1698
+ ms,
1699
+ durationMs: Number(entry.durationMs ?? 0),
1700
+ totalIssues: Number(md.totalIssues),
1701
+ });
1702
+ state.latency.knip.set(pid, rows);
1703
+ }
1704
+ } else if (phase === "degradation_ledger") {
1705
+ const kind = md.kind ?? "unknown";
1706
+ const perPid = state.latency.degradationKinds.get(pid) ?? new Map();
1707
+ perPid.set(kind, Math.max(perPid.get(kind) ?? 0, Number(md.count ?? 0)));
1708
+ state.latency.degradationKinds.set(pid, perPid);
1709
+ if (kind === "hook-await-exceeded") {
1710
+ const budgetMs = Number(md.budgetMs);
1711
+ const elapsedMs = Number(md.elapsedMs);
1712
+ state.latency.hookAwaitExceeded.push({
1713
+ ts: nowIso,
1714
+ pid,
1715
+ hook: md.hook,
1716
+ label: md.label,
1717
+ budgetMs,
1718
+ elapsedMs,
1719
+ cause: md.cause,
1720
+ ratio: budgetMs > 0 ? elapsedMs / budgetMs : 0,
1721
+ });
1722
+ }
1723
+ } else if (phase === "lsp_client_wait_timeout") {
1724
+ const serverIds = Array.isArray(md.serverIds) ? md.serverIds : [];
1725
+ if (serverIds.length === 0) {
1726
+ const cur = state.latency.lspWaitEmpty.get(pid) ?? { ms: 0, rows: 0 };
1727
+ cur.ms += Number(entry.durationMs ?? 0);
1728
+ cur.rows += 1;
1729
+ state.latency.lspWaitEmpty.set(pid, cur);
1730
+ }
1731
+ } else if (phase === "lsp_touch_file") {
1732
+ const cur = state.latency.lspTouchEdit.get(pid) ?? {
1733
+ rows: 0,
1734
+ noClients: 0,
1735
+ };
1736
+ if (md.source === "tool_call:edit") {
1737
+ cur.rows += 1;
1738
+ if (md.failureKind === "no_clients_none_spawning") cur.noClients += 1;
1739
+ }
1740
+ state.latency.lspTouchEdit.set(pid, cur);
1741
+ if (
1742
+ md.source === "tool_call:edit" &&
1743
+ md.failureKind === "no_clients_none_spawning"
1744
+ ) {
1745
+ state.latency.lspTouchNoClients.push({
1746
+ ts: nowIso,
1747
+ ms,
1748
+ pid,
1749
+ filePath: entry.filePath,
1750
+ });
1751
+ }
1752
+ } else if (phase === "lsp_client_selected") {
1753
+ if (md.outcome === "warm-reuse") {
1754
+ state.latency.lspClientSelectedWarm.push({
1755
+ ts: nowIso,
1756
+ ms,
1757
+ pid,
1758
+ filePath: entry.filePath,
1759
+ });
1760
+ }
1761
+ } else if (phase === "read_guard_branch_retained") {
1762
+ state.latency.carryRestart.push({
1763
+ ts: nowIso,
1764
+ pid,
1765
+ trigger: md.trigger,
1766
+ source: md.source,
1767
+ kept: Number(md.kept ?? 0),
1768
+ dropped: Number(md.dropped ?? 0),
1769
+ branchToolResults: Number(md.branchToolResults ?? 0),
1770
+ branchReadable: md.branchReadable === true,
1771
+ });
1772
+ } else if (phase === "agent_nudge") {
1773
+ state.latency.agentNudges.push({
1774
+ ts: nowIso,
1775
+ ms,
1776
+ pid,
1777
+ originCrossProcess: Number(md.originCrossProcess ?? 0),
1778
+ originLocal: Number(md.originLocal ?? 0),
1779
+ reasonMix: md.reasonMix,
1780
+ });
1781
+ } else if (phase === "session_scope_transition") {
1782
+ state.latency.sessionScopeTransitions.push({
1783
+ ts: nowIso,
1784
+ ms,
1785
+ pid,
1786
+ handoffSource: md.handoffSource,
1787
+ sessionId: md.sessionId,
1788
+ });
1789
+ } else if (phase === "late_runner_findings") {
1790
+ state.latency.lateRunnerFindings.push({
1791
+ ts: nowIso,
1792
+ ms,
1793
+ pid,
1794
+ turnId: entry.turnId,
1795
+ failed: Number(md.failed ?? 0),
1796
+ delivered: Number(md.delivered ?? 0),
1797
+ dropped: Number(md.dropped ?? 0),
1798
+ pending: Number(md.pending ?? 0),
1799
+ });
1800
+ } else if (phase === "late_auxiliary_findings") {
1801
+ const pairs = Array.isArray(md.stuckPairs) ? md.stuckPairs : [];
1802
+ for (const pair of pairs) {
1803
+ if (!pair?.filePath || !pair?.serverId) continue;
1804
+ const key = `${pid}\u0000${pair.filePath}\u0000${pair.serverId}`;
1805
+ state.latency.auxStuck.set(
1806
+ key,
1807
+ (state.latency.auxStuck.get(key) ?? 0) + 1,
1808
+ );
1809
+ }
1810
+ } else if (phase === "advisory_provenance_decision") {
1811
+ const reasons = Array.isArray(md.reasons) ? md.reasons : [];
1812
+ // The malformed reason implies decision "historical": the validator
1813
+ // returns status "unknown" with it (clients/advisory-provenance.ts) and
1814
+ // the row logs every non-current status as historical
1815
+ // (clients/runtime-context.ts), so the reason alone is the rule.
1816
+ if (reasons.includes("malformed-or-legacy-provenance")) {
1817
+ state.latency.advisoryProvenance.push({
1818
+ ts: nowIso,
1819
+ ms,
1820
+ pid,
1821
+ reasons,
1822
+ provenanceStamp: md.provenanceStamp,
1823
+ advisoryKind: md.advisoryKind,
1824
+ });
1825
+ }
1826
+ }
1827
+ }
1828
+
1829
+ /** D2: extension.log rows that name a test home. */
1830
+ async function analyzeExtension(files, state) {
1831
+ for (const file of files) {
1832
+ await forEachJsonLine(file, "extension", state, (entry) => {
1833
+ const ts = dateOf(entry.ts);
1834
+ if (!inWindow(ts)) return;
1835
+ state.seen.inc("extension");
1836
+ if (SCRATCH_PATH_RE.test(JSON.stringify(entry))) {
1837
+ const pid = String(entry.pid ?? "unknown");
1838
+ state.extension.scratchRows.set(
1839
+ pid,
1840
+ (state.extension.scratchRows.get(pid) ?? 0) + 1,
1841
+ );
1842
+ }
1843
+ const level = String(entry.level ?? "").toLowerCase();
1844
+ if (level === "warn" || level === "error") {
1845
+ const message = String(entry.message ?? "").slice(0, 80);
1846
+ state.extension.warnErrorGroups.inc(
1847
+ `${entry.subsystem ?? "unknown"}: ${message}`,
1848
+ );
1849
+ }
1850
+ });
1851
+ }
1852
+ }
1853
+
1854
+ function countBetween(sorted, a, b) {
1855
+ let n = 0;
1856
+ for (const value of sorted) {
1857
+ if (value > a && value < b) n += 1;
1858
+ else if (value >= b) break;
1859
+ }
1860
+ return n;
1861
+ }
1862
+
1863
+ /** D1: latency gaps >= 10 min that a live session filled with >= 20 rows. */
1864
+ function computeCoverageGaps(state) {
1865
+ const lat = [...state.latency.rowTs].sort((a, b) => a - b);
1866
+ const ss = [...state.session.rowTs].sort((a, b) => a - b);
1867
+ const cas = [...state.latency.cascadeTs].sort((a, b) => a - b);
1868
+ const gaps = [];
1869
+ for (let i = 1; i < lat.length; i += 1) {
1870
+ const a = lat[i - 1];
1871
+ const b = lat[i];
1872
+ const minutes = (b - a) / 60000;
1873
+ if (minutes < 10) continue;
1874
+ const sessionstartRows = countBetween(ss, a, b);
1875
+ if (sessionstartRows < 20) continue;
1876
+ gaps.push({
1877
+ start: new Date(a).toISOString(),
1878
+ end: new Date(b).toISOString(),
1879
+ minutes: Math.round(minutes),
1880
+ sessionstartRows,
1881
+ cascadeRows: countBetween(cas, a, b),
1882
+ });
1883
+ }
1884
+ return gaps;
1885
+ }
1886
+
1887
+ /** D5: per-session firing/stale text and per-session delivery outcomes. */
1888
+ function computeTestRunnerHealth(state) {
1889
+ const runs = state.session.runs.map((r) => ({
1890
+ startTs: r.startTs,
1891
+ firings: r.testFirings,
1892
+ stale: r.testStale,
1893
+ }));
1894
+ const staleRuns = runs.filter(
1895
+ (r) => r.firings >= 10 && r.stale / r.firings >= 0.5,
1896
+ );
1897
+ const firings = runs.reduce((n, r) => n + r.firings, 0);
1898
+ const stale = runs.reduce((n, r) => n + r.stale, 0);
1899
+ const sessions = [];
1900
+ for (const [sessionId, c] of state.latency.testRunnerDelivery) {
1901
+ const total = c.entries().reduce((n, [, v]) => n + v, 0);
1902
+ const delivered = c.get("delivered");
1903
+ sessions.push({
1904
+ sessionId,
1905
+ total,
1906
+ staged: c.get("staged"),
1907
+ eligible: c.get("eligible"),
1908
+ delivered,
1909
+ superseded: c.get("superseded"),
1910
+ deliveredShare: total ? delivered / total : 0,
1911
+ });
1912
+ }
1913
+ const lowDelivery = sessions.filter(
1914
+ (s) => s.total >= 10 && s.deliveredShare < 0.25,
1915
+ );
1916
+ return {
1917
+ firings,
1918
+ stale,
1919
+ staleShare: firings ? stale / firings : 0,
1920
+ staleRuns,
1921
+ sessions,
1922
+ lowDelivery,
1923
+ };
1924
+ }
1925
+
1926
+ /** D6: scanner issue-count drift and turn-end knip wall cost, per pid. */
1927
+ function computeKnip(state) {
1928
+ const drift = [];
1929
+ const cost = [];
1930
+ for (const [pid, rowsRaw] of state.latency.knip) {
1931
+ const rows = [...rowsRaw]
1932
+ .filter((r) => r.ms != null)
1933
+ .sort((a, b) => a.ms - b.ms);
1934
+ if (rows.length >= 10) {
1935
+ const issues = rows.map((r) => r.totalIssues);
1936
+ let increments = 0;
1937
+ for (let i = 1; i < issues.length; i += 1)
1938
+ if (issues[i] - issues[i - 1] >= 100) increments += 1;
1939
+ const min = Math.min(...issues);
1940
+ const max = Math.max(...issues);
1941
+ const spread = min > 0 ? (max - min) / min : 0;
1942
+ if (increments >= 5 || spread >= 0.25)
1943
+ drift.push({ pid, rows: rows.length, increments, min, max, spread });
1944
+ }
1945
+ const first = state.latency.pidFirstTs.get(pid);
1946
+ const last = state.latency.pidLastTs.get(pid);
1947
+ const hours = first != null && last != null ? (last - first) / 3600000 : 0;
1948
+ const totalMs = rows.reduce((n, r) => n + r.durationMs, 0);
1949
+ const maxRow = rows.reduce((n, r) => Math.max(n, r.durationMs), 0);
1950
+ // A lifetime under an hour counts as one hour, so a short pid's few
1951
+ // seconds of knip cannot extrapolate to a large hourly rate.
1952
+ const perHour = totalMs / Math.max(hours, 1);
1953
+ if (perHour >= 30000 || maxRow >= 5000)
1954
+ cost.push({ pid, rows: rows.length, totalMs, hours, perHour, maxRow });
1955
+ }
1956
+ return { drift, cost };
1957
+ }
1958
+
1959
+ /** D7: hook deopts that overran their budget, plus the degradation census. */
1960
+ function computeHookAwait(state) {
1961
+ const rows = [...state.latency.hookAwaitExceeded].sort((a, b) =>
1962
+ String(b.ts).localeCompare(String(a.ts)),
1963
+ );
1964
+ const census = [];
1965
+ for (const [pid, kinds] of state.latency.degradationKinds) {
1966
+ census.push({
1967
+ pid,
1968
+ kinds: [...kinds.entries()]
1969
+ .sort((a, b) => b[1] - a[1])
1970
+ .slice(0, 12)
1971
+ .map(([kind, count]) => `${kind}=${count}`),
1972
+ });
1973
+ }
1974
+ return { rows, over: rows.filter((r) => r.ratio >= 1.1).length, census };
1975
+ }
1976
+
1977
+ /** D8: turn_end hook duration per pid, and the retained-turn-state runs. */
1978
+ function computeTurnEndSlow(state) {
1979
+ const flagged = [];
1980
+ for (const [pid, rows] of state.latency.turnEndTools) {
1981
+ const slow = rows.filter((r) => r.durationMs > 3000).length;
1982
+ const max = rows.reduce((n, r) => Math.max(n, r.durationMs), 0);
1983
+ const share = rows.length ? slow / rows.length : 0;
1984
+ if ((rows.length >= 20 && share >= 0.1) || max >= 8000)
1985
+ flagged.push({ pid, rows: rows.length, slow, share, max });
1986
+ }
1987
+ const retainRuns = state.session.runs
1988
+ .filter((r) => r.retainingNewerTurn >= 5)
1989
+ .map((r) => ({ start: r.startTs, count: r.retainingNewerTurn }));
1990
+ return { flagged, retainRuns };
1991
+ }
1992
+
1993
+ /** D9: empty-candidate waits, edit touches with no client, warm-reuse clashes. */
1994
+ function computeLspWait(state) {
1995
+ const empty = [];
1996
+ for (const [pid, cur] of state.latency.lspWaitEmpty)
1997
+ if (cur.ms >= 5000) empty.push({ pid, ms: cur.ms, rows: cur.rows });
1998
+ const noClients = [];
1999
+ for (const [pid, cur] of state.latency.lspTouchEdit)
2000
+ if (cur.rows >= 20 && cur.noClients / cur.rows >= 0.2)
2001
+ noClients.push({
2002
+ pid,
2003
+ rows: cur.rows,
2004
+ noClients: cur.noClients,
2005
+ share: cur.noClients / cur.rows,
2006
+ });
2007
+ const contradictions = [];
2008
+ for (const touch of state.latency.lspTouchNoClients) {
2009
+ const match = state.latency.lspClientSelectedWarm.find(
2010
+ (w) =>
2011
+ w.pid === touch.pid &&
2012
+ w.filePath === touch.filePath &&
2013
+ touch.ms != null &&
2014
+ w.ms != null &&
2015
+ Math.abs(w.ms - touch.ms) <= 500,
2016
+ );
2017
+ if (match)
2018
+ contradictions.push({
2019
+ pid: touch.pid,
2020
+ filePath: touch.filePath,
2021
+ touchTs: touch.ts,
2022
+ selectedTs: match.ts,
2023
+ });
2024
+ }
2025
+ const pids = new Set([
2026
+ ...empty.map((e) => e.pid),
2027
+ ...noClients.map((e) => e.pid),
2028
+ ...contradictions.map((c) => c.pid),
2029
+ ]);
2030
+ return { empty, noClients, contradictions, pidCount: pids.size };
2031
+ }
2032
+
2033
+ /** D11: a restart kept nothing while the branch held a populated read set. */
2034
+ function computeCarryRestart(state) {
2035
+ return state.latency.carryRestart.filter(
2036
+ (r) =>
2037
+ ["startup", "resume", "fork", "reload"].includes(r.trigger) &&
2038
+ r.source !== "none" &&
2039
+ r.kept + r.dropped === 0 &&
2040
+ r.branchToolResults >= 50 &&
2041
+ r.branchReadable,
2042
+ );
2043
+ }
2044
+
2045
+ /** D12: a cross-process nudge right after a sidecar handoff (suspect grade). */
2046
+ function computeRestartSelfNudge(state) {
2047
+ const flags = [];
2048
+ for (const nudge of state.latency.agentNudges) {
2049
+ if (nudge.originCrossProcess < 1) continue;
2050
+ const transition = state.latency.sessionScopeTransitions.find(
2051
+ (t) =>
2052
+ t.pid === nudge.pid &&
2053
+ ["own-sidecar", "parent-sidecar"].includes(t.handoffSource) &&
2054
+ nudge.ms != null &&
2055
+ t.ms != null &&
2056
+ nudge.ms >= t.ms &&
2057
+ nudge.ms - t.ms <= 300000,
2058
+ );
2059
+ if (transition) flags.push({ ...nudge, transitionTs: transition.ts });
2060
+ }
2061
+ return flags;
2062
+ }
2063
+
2064
+ /** D13: a collect-later runner failed with findings and nothing was delivered. */
2065
+ function computeDeferredRunner(state) {
2066
+ const flags = [];
2067
+ for (const runner of state.latency.deferredRunnerFailed) {
2068
+ const delivery = state.latency.lateRunnerFindings.find(
2069
+ (f) =>
2070
+ f.pid === runner.pid &&
2071
+ f.turnId === runner.turnId &&
2072
+ (runner.ms == null || f.ms == null || f.ms >= runner.ms),
2073
+ );
2074
+ if (
2075
+ delivery &&
2076
+ delivery.failed >= 1 &&
2077
+ delivery.delivered === 0 &&
2078
+ delivery.dropped === 0
2079
+ )
2080
+ flags.push({
2081
+ ...runner,
2082
+ deliveryTs: delivery.ts,
2083
+ pending: delivery.pending,
2084
+ });
2085
+ }
2086
+ return flags;
2087
+ }
2088
+
2089
+ /** D14: an auxiliary (file, server) pair stuck in two or more turn ends. */
2090
+ function computeAuxStuck(state) {
2091
+ const flagged = [];
2092
+ for (const [key, count] of state.latency.auxStuck) {
2093
+ if (count < 2) continue;
2094
+ const [pid, filePath, serverId] = key.split("\u0000");
2095
+ flagged.push({ pid, filePath, serverId, count });
2096
+ }
2097
+ flagged.sort((a, b) => b.count - a.count);
2098
+ return flagged;
2099
+ }
2100
+
2101
+ /** D15: `historical` provenance with a malformed/legacy stamp. */
2102
+ function computeAdvisoryProvenance(state) {
2103
+ return state.latency.advisoryProvenance.map((row) => {
2104
+ const first = state.latency.pidFirstTs.get(row.pid);
2105
+ const seconds =
2106
+ first != null && row.ms != null
2107
+ ? Math.round((row.ms - first) / 1000)
2108
+ : null;
2109
+ return { ...row, secondsSinceFirstRow: seconds };
2110
+ });
2111
+ }
2112
+
2113
+ /** D16: `pi-lens loaded: Nms` loads over 2 s. A load with no
2114
+ * `session_start fired` within the next 60 s is a short-lived pid. */
2115
+ function computeSlowExtensionLoad(state) {
2116
+ const fired = state.session.firedTs;
2117
+ return state.session.slowLoads
2118
+ .filter((l) => l.durationMs >= 2000)
2119
+ .map((l) => ({
2120
+ ...l,
2121
+ shortLived: !(
2122
+ Number.isFinite(l.ms) &&
2123
+ fired.some((f) => f >= l.ms && f - l.ms <= 60000)
2124
+ ),
2125
+ }));
2126
+ }
2127
+
1298
2128
  function buildReport(state) {
1299
2129
  const smells = [];
1300
2130
  const smellCount = (id) => state.smellTotals.get(id);
2131
+
2132
+ // D1-D16 detector computations. Kept out of the smell list so every value
2133
+ // they produce is assertable on its own, not only through a smell count.
2134
+ const coverageGaps = computeCoverageGaps(state);
2135
+ const testRunnerHealth = computeTestRunnerHealth(state);
2136
+ const knip = computeKnip(state);
2137
+ const hookAwait = computeHookAwait(state);
2138
+ const turnEndSlow = computeTurnEndSlow(state);
2139
+ const lspWait = computeLspWait(state);
2140
+ const carryRestart = computeCarryRestart(state);
2141
+ const restartSelfNudge = computeRestartSelfNudge(state);
2142
+ const deferredRunner = computeDeferredRunner(state);
2143
+ const auxStuck = computeAuxStuck(state);
2144
+ const advisoryProvenance = computeAdvisoryProvenance(state);
2145
+ const slowExtensionLoad = computeSlowExtensionLoad(state);
2146
+ const latencyScratch = [...state.latency.scratchRows.values()].reduce(
2147
+ (n, v) => n + v,
2148
+ 0,
2149
+ );
2150
+ const extensionScratch = [...state.extension.scratchRows.values()].reduce(
2151
+ (n, v) => n + v,
2152
+ 0,
2153
+ );
2154
+ const d3Runs = state.session.runs.filter((r) => {
2155
+ const excludedTest = r.excludedTestFiles.some((p) =>
2156
+ /\.(test|spec)\.|\/tests?\//.test(p),
2157
+ );
2158
+ return (r.excluded >= 1 && excludedTest) || (r.edits >= 10 && r.ran === 0);
2159
+ });
2160
+ const d4Cross = state.session.runs.reduce(
2161
+ (n, r) => n + (r.crossCheckout >= 3 ? r.crossCheckout : 0),
2162
+ 0,
2163
+ );
2164
+ const testRunnerSmellCount =
2165
+ testRunnerHealth.staleRuns.length + testRunnerHealth.lowDelivery.length;
1301
2166
  addSmell(
1302
2167
  smells,
1303
2168
  "diagnostic-blockers",
@@ -1407,19 +2272,17 @@ function buildReport(state) {
1407
2272
  );
1408
2273
  addSmell(
1409
2274
  smells,
1410
- "read-guard-friction",
1411
- smellCount("read-guard-friction"),
1412
- "Read-guard blocked/warned edits or exact replacement misses",
1413
- [...state.readGuard.blocked, ...state.readGuard.oldTextIssues].slice(
1414
- 0,
1415
- limit,
1416
- ),
2275
+ "read-guard-blocks",
2276
+ state.readGuard.events.get("edit_blocked") +
2277
+ state.readGuard.events.get("edit_preflight_blocked"),
2278
+ "Read-guard blocked edits; host/model provenance is not present on the block rows, and warns and exact-replacement misses are informational",
2279
+ state.readGuard.blocks.slice(0, limit),
1417
2280
  );
1418
2281
  addSmell(
1419
2282
  smells,
1420
2283
  "read-guard-stale-ranges",
1421
2284
  smellCount("read-guard-stale-ranges"),
1422
- "Covered edit ranges whose read snapshot no longer matched current content",
2285
+ `Enforced range mismatches whose read snapshot no longer matched (bypassed-content-match ${state.readGuard.bypassedMismatch} excluded as an allowed edit)`,
1423
2286
  state.readGuard.staleRanges.slice(0, limit),
1424
2287
  );
1425
2288
  addSmell(
@@ -1479,6 +2342,258 @@ function buildReport(state) {
1479
2342
  ws.sweeps.slice(0, limit),
1480
2343
  );
1481
2344
 
2345
+ // D1: a latency gap a live session filled.
2346
+ addSmell(
2347
+ smells,
2348
+ "log-coverage-gap",
2349
+ coverageGaps.length,
2350
+ "Latency stream gap >= 10 min that a live session filled with >= 20 sessionstart rows",
2351
+ coverageGaps.slice(0, limit).map((g) => ({
2352
+ ts: g.start,
2353
+ message: `${g.minutes} min gap ${g.start} -> ${g.end}; sessionstartRows=${g.sessionstartRows}; cascadeRows=${g.cascadeRows}`,
2354
+ })),
2355
+ );
2356
+ // D2: test home markers in a real log. Never added to the
2357
+ // default denylist so the pollution stays visible.
2358
+ addSmell(
2359
+ smells,
2360
+ "real-log-test-pollution",
2361
+ latencyScratch + extensionScratch,
2362
+ `Rows naming a test home (witness-home, pi-lens-test-*) (latency ${latencyScratch}, extension ${extensionScratch}); these never belong in a real log`,
2363
+ [
2364
+ ...[...state.latency.scratchRows.entries()].map(([pid, count]) => ({
2365
+ key: `latency pid ${pid}`,
2366
+ count,
2367
+ })),
2368
+ ...[...state.extension.scratchRows.entries()].map(([pid, count]) => ({
2369
+ key: `extension pid ${pid}`,
2370
+ count,
2371
+ })),
2372
+ ].slice(0, limit),
2373
+ );
2374
+ addSmell(
2375
+ smells,
2376
+ "extension-warn-errors",
2377
+ state.extension.warnErrorGroups.size,
2378
+ "extension.log warn/error groups by subsystem and message prefix",
2379
+ state.extension.warnErrorGroups.top(limit),
2380
+ );
2381
+ // D3: a run that excluded its own test files, or edited without running one.
2382
+ addSmell(
2383
+ smells,
2384
+ "turn-end-tests-excluded",
2385
+ d3Runs.length,
2386
+ "Turn-end runs that skipped test files by policy, or edited >= 10 files and ran no test",
2387
+ d3Runs.slice(0, limit).map((r) => ({
2388
+ ts: r.startTs,
2389
+ message: `edits=${r.edits} ran=${r.ran} excluded=${r.excluded} noTestFile=${r.noTestFile}${
2390
+ r.excludedTestFiles.length ? `: ${r.excludedTestFiles.join(", ")}` : ""
2391
+ }`,
2392
+ })),
2393
+ );
2394
+ // D4: a failed-first target that resolved to a different checkout (#3649).
2395
+ addSmell(
2396
+ smells,
2397
+ "test-target-cross-checkout",
2398
+ d4Cross,
2399
+ "Failed-first test targets resolved to a different checkout than the edited source (regression guard for #3649)",
2400
+ state.session.runs
2401
+ .filter((r) => r.crossCheckout >= 3)
2402
+ .slice(0, limit)
2403
+ .map((r) => ({
2404
+ ts: r.startTs,
2405
+ message: `${r.crossCheckout} cross-checkout of ${r.failedFirst} failed-first`,
2406
+ })),
2407
+ );
2408
+ // D5: sessions whose turn-end verdicts went stale, or were rarely delivered.
2409
+ addSmell(
2410
+ smells,
2411
+ "test-runner-stale-verdicts",
2412
+ testRunnerSmellCount,
2413
+ `Sessions with >= 10 test firings and >= 50% stale (${testRunnerHealth.staleRuns.length}), or >= 10 delivery rows and < 25% delivered (${testRunnerHealth.lowDelivery.length}); window firings ${testRunnerHealth.firings}, stale ${testRunnerHealth.stale}`,
2414
+ [
2415
+ ...testRunnerHealth.staleRuns.map((r) => ({
2416
+ ts: r.startTs,
2417
+ count: r.stale,
2418
+ message: `firings=${r.firings} stale=${r.stale}`,
2419
+ })),
2420
+ ...testRunnerHealth.lowDelivery.map((s) => ({
2421
+ key: s.sessionId,
2422
+ count: s.delivered,
2423
+ message: `delivered ${s.delivered}/${s.total} (${Math.round(
2424
+ s.deliveredShare * 100,
2425
+ )}%), staged ${s.staged}, eligible ${s.eligible}, superseded ${s.superseded}`,
2426
+ })),
2427
+ ].slice(0, limit),
2428
+ );
2429
+ // D6: scanner-count drift and knip wall cost.
2430
+ addSmell(
2431
+ smells,
2432
+ "scanner-count-drift",
2433
+ knip.drift.length,
2434
+ "knip totalIssues rose by >= 100 between consecutive executed runs at least 5 times, or its spread >= 25% (pids with >= 10 executed runs)",
2435
+ knip.drift.slice(0, limit).map((d) => ({
2436
+ key: `pid ${d.pid}`,
2437
+ count: d.increments,
2438
+ message: `issues ${d.min} -> ${d.max} (+${Math.round(
2439
+ d.spread * 100,
2440
+ )}%), ${d.increments} increments >= 100`,
2441
+ })),
2442
+ );
2443
+ addSmell(
2444
+ smells,
2445
+ "turn-end-knip-cost",
2446
+ knip.cost.length,
2447
+ "knip consumed >= 30 s per hour of pid lifetime (a lifetime under an hour counts as one hour), or a single run took >= 5 s",
2448
+ knip.cost.slice(0, limit).map((c) => ({
2449
+ key: `pid ${c.pid}`,
2450
+ count: Math.round(c.perHour),
2451
+ message: `${c.totalMs}ms over ${c.hours.toFixed(2)}h = ${Math.round(
2452
+ c.perHour,
2453
+ )}ms/h, max ${c.maxRow}ms`,
2454
+ })),
2455
+ );
2456
+ // D7: hook deopts that overran their budget.
2457
+ addSmell(
2458
+ smells,
2459
+ "hook-await-exceeded",
2460
+ hookAwait.rows.length,
2461
+ `Hook awaits that overran their budget (${hookAwait.over} with ratio >= 1.1)`,
2462
+ hookAwait.rows.slice(0, limit).map((r) => ({
2463
+ ts: r.ts,
2464
+ message: `${r.hook}:${r.label} ${r.elapsedMs}/${r.budgetMs}ms (${r.ratio.toFixed(
2465
+ 3,
2466
+ )}) pid=${r.pid} cause=${r.cause}`,
2467
+ })),
2468
+ );
2469
+ // D8: turn_end hook duration and retained-state churn.
2470
+ addSmell(
2471
+ smells,
2472
+ "turn-end-slow",
2473
+ turnEndSlow.flagged.length,
2474
+ "turn_end hook duration over budget (a pid with >= 10% of summaries > 3 s, or one >= 8 s)",
2475
+ turnEndSlow.flagged.slice(0, limit).map((f) => ({
2476
+ key: `pid ${f.pid}`,
2477
+ count: f.slow,
2478
+ message: `${f.slow}/${f.rows} over 3s (${Math.round(
2479
+ f.share * 100,
2480
+ )}%), max ${f.max}ms`,
2481
+ })),
2482
+ );
2483
+ addSmell(
2484
+ smells,
2485
+ "turn-end-retained-state",
2486
+ turnEndSlow.retainRuns.length,
2487
+ "Turn-end retained newer turn state at least 5 times in one session",
2488
+ turnEndSlow.retainRuns.slice(0, limit).map((r) => ({
2489
+ ts: r.start,
2490
+ count: r.count,
2491
+ })),
2492
+ );
2493
+ // D9: empty-candidate waits and no-client/edit warm-reuse contradictions.
2494
+ addSmell(
2495
+ smells,
2496
+ "lsp-wait-empty-candidates",
2497
+ lspWait.pidCount,
2498
+ `LSP touch saw no client while a warm client was selected, or waited with an empty candidate set (empty ${lspWait.empty.length}, noClients ${lspWait.noClients.length}, contradictions ${lspWait.contradictions.length})`,
2499
+ [
2500
+ ...lspWait.empty.map((e) => ({
2501
+ key: `pid ${e.pid}`,
2502
+ count: e.rows,
2503
+ message: `${e.ms}ms waiting with no candidate server`,
2504
+ })),
2505
+ ...lspWait.noClients.map((e) => ({
2506
+ key: `pid ${e.pid}`,
2507
+ count: Math.round(e.share * 100),
2508
+ message: `${e.noClients}/${e.rows} edits saw no client (${Math.round(
2509
+ e.share * 100,
2510
+ )}%)`,
2511
+ })),
2512
+ ...lspWait.contradictions.map((c) => ({
2513
+ key: `pid ${c.pid}`,
2514
+ count: 1,
2515
+ message: `${c.filePath}: no-client touch at ${c.touchTs}, warm-reuse at ${c.selectedTs}`,
2516
+ })),
2517
+ ].slice(0, limit),
2518
+ );
2519
+ // D10: a zero_read block that contradicts earlier read/edit evidence.
2520
+ addSmell(
2521
+ smells,
2522
+ "resume-state-loss",
2523
+ state.readGuard.stateLost.length,
2524
+ `zero_read edits blocked despite earlier read/edit evidence in the same session (genuine ${state.readGuard.genuineZeroRead.length})`,
2525
+ state.readGuard.stateLost.slice(0, limit),
2526
+ );
2527
+ // D11: a restart whose read-set carry was empty while the branch was populated.
2528
+ addSmell(
2529
+ smells,
2530
+ "carry-empty-restart",
2531
+ carryRestart.length,
2532
+ "Restart carry kept and dropped nothing while the branch held a populated read set",
2533
+ carryRestart.slice(0, limit).map((r) => ({
2534
+ key: `pid ${r.pid}`,
2535
+ count: r.branchToolResults,
2536
+ message: `trigger=${r.trigger} source=${r.source} kept=${r.kept} dropped=${r.dropped} branchToolResults=${r.branchToolResults}`,
2537
+ })),
2538
+ );
2539
+ // D12: a cross-process nudge right after a sidecar handoff (suspect until O7).
2540
+ addSmell(
2541
+ smells,
2542
+ "restart-self-nudge",
2543
+ restartSelfNudge.length,
2544
+ "Cross-process nudge within 300 s of a sidecar handoff (suspect-grade until the nudge carries a file id, O7)",
2545
+ restartSelfNudge.slice(0, limit).map((r) => ({
2546
+ key: `pid ${r.pid}`,
2547
+ count: r.originCrossProcess,
2548
+ message: `${JSON.stringify(r.reasonMix)} crossProcess=${r.originCrossProcess} after ${r.transitionTs}`,
2549
+ })),
2550
+ );
2551
+ // D13: a collect-later runner failed with findings and none were delivered.
2552
+ addSmell(
2553
+ smells,
2554
+ "deferred-runner-failed-undelivered",
2555
+ deferredRunner.length,
2556
+ "collect-later runner failed with findings and the delivery row shows failed>=1, delivered=0, dropped=0 (#3796 witness)",
2557
+ deferredRunner.slice(0, limit).map((r) => ({
2558
+ ts: r.ts,
2559
+ message: `${r.runnerId} pid=${r.pid} turnId=${r.turnId} diagnostics=${r.diagnosticCount} delivery=${r.deliveryTs}`,
2560
+ })),
2561
+ );
2562
+ // D14: an auxiliary (file, server) pair stuck in >= 2 turn ends.
2563
+ addSmell(
2564
+ smells,
2565
+ "aux-stuck-pair",
2566
+ auxStuck.length,
2567
+ "Auxiliary (file, server) pair stuck in >= 2 turn ends",
2568
+ auxStuck.slice(0, limit).map((p) => ({
2569
+ key: p.serverId,
2570
+ count: p.count,
2571
+ message: `${p.filePath} pid=${p.pid}`,
2572
+ })),
2573
+ );
2574
+ // D15: historical provenance with a malformed/legacy stamp.
2575
+ addSmell(
2576
+ smells,
2577
+ "advisory-provenance-unknown",
2578
+ advisoryProvenance.length,
2579
+ "Advisory provenance resolved to historical with a malformed-or-legacy stamp",
2580
+ advisoryProvenance.slice(0, limit).map((r) => ({
2581
+ ts: r.ts,
2582
+ message: `${r.provenanceStamp} (${r.advisoryKind}) ${r.secondsSinceFirstRow}s after pid start`,
2583
+ })),
2584
+ );
2585
+ // D16: extension load time over 2 s.
2586
+ addSmell(
2587
+ smells,
2588
+ "slow-extension-load",
2589
+ slowExtensionLoad.length,
2590
+ "pi-lens loaded >= 2000ms after process start (short-lived: no session_start fired within 60 s)",
2591
+ slowExtensionLoad.slice(0, limit).map((l) => ({
2592
+ ...l,
2593
+ message: `${l.message}${l.shortLived ? " [short-lived]" : ""}`,
2594
+ })),
2595
+ );
2596
+
1482
2597
  return {
1483
2598
  window: state.window,
1484
2599
  filesScanned: Object.fromEntries(
@@ -1549,6 +2664,12 @@ function buildReport(state) {
1549
2664
  0,
1550
2665
  limit,
1551
2666
  ),
2667
+ // E2/D10: the block/warn split and the lost-read-set classification.
2668
+ warns: state.readGuard.warns.slice(0, limit),
2669
+ blockByKind: state.readGuard.blockByKind.toJSON(),
2670
+ bypassedMismatch: state.readGuard.bypassedMismatch,
2671
+ stateLost: state.readGuard.stateLost.slice(0, limit),
2672
+ genuineZeroRead: state.readGuard.genuineZeroRead.slice(0, limit),
1552
2673
  },
1553
2674
  treeSitter: {
1554
2675
  phases: state.treeSitter.phases.toJSON(),
@@ -1559,7 +2680,8 @@ function buildReport(state) {
1559
2680
  session: {
1560
2681
  starts: state.session.starts,
1561
2682
  cwds: state.session.cwds.top(limit),
1562
- rotations: state.session.rotations.toJSON(),
2683
+ commits: state.session.commits.toJSON(),
2684
+ slowLoads: state.session.slowLoads.slice(0, limit),
1563
2685
  errors: state.session.errors.slice(0, limit),
1564
2686
  },
1565
2687
  // #2526: the positive-observability counters behind the
@@ -1602,6 +2724,43 @@ function buildReport(state) {
1602
2724
  .sort((a, b) => b.total - a.total)
1603
2725
  .slice(0, limit * 3),
1604
2726
  },
2727
+ // D1-D16: the per-detector values behind the smells above, so a test can
2728
+ // pin the rule (not only the rendered count) and a reader can see WHY.
2729
+ detectors: {
2730
+ logCoverageGap: { gaps: coverageGaps },
2731
+ realLogTestPollution: {
2732
+ latencyByPid: Object.fromEntries(state.latency.scratchRows),
2733
+ extensionByPid: Object.fromEntries(state.extension.scratchRows),
2734
+ latencyTotal: latencyScratch,
2735
+ extensionTotal: extensionScratch,
2736
+ },
2737
+ turnEndTestsExcluded: {
2738
+ runs: d3Runs.map((r) => ({ ...r })),
2739
+ },
2740
+ testTargetCrossCheckout: {
2741
+ runs: state.session.runs.map((r) => ({
2742
+ startTs: r.startTs,
2743
+ crossCheckout: r.crossCheckout,
2744
+ failedFirst: r.failedFirst,
2745
+ })),
2746
+ },
2747
+ testRunnerStaleVerdicts: testRunnerHealth,
2748
+ knip,
2749
+ hookAwait,
2750
+ turnEndSlow,
2751
+ lspWait,
2752
+ resumeStateLoss: {
2753
+ stateLost: state.readGuard.stateLost,
2754
+ genuine: state.readGuard.genuineZeroRead,
2755
+ },
2756
+ carryEmptyRestart: carryRestart,
2757
+ restartSelfNudge,
2758
+ deferredRunnerFailedUndelivered: deferredRunner,
2759
+ auxStuckPairs: auxStuck,
2760
+ advisoryProvenanceUnknown: advisoryProvenance,
2761
+ slowExtensionLoad,
2762
+ extensionWarnErrors: state.extension.warnErrorGroups.toJSON(),
2763
+ },
1605
2764
  };
1606
2765
  }
1607
2766
 
@@ -1649,6 +2808,60 @@ function printReport(report) {
1649
2808
  }
1650
2809
  }
1651
2810
 
2811
+ // D1/D2/D5/D6/D7: the non-smell breakdown behind the new detectors.
2812
+ const det = report.detectors ?? {};
2813
+ const gap = det.logCoverageGap;
2814
+ if (gap?.gaps?.length) {
2815
+ console.log("\nLog coverage");
2816
+ for (const g of gap.gaps ?? [])
2817
+ console.log(
2818
+ ` gap ${g.minutes}min ${g.start} → ${g.end} sessionstart=${g.sessionstartRows} cascade=${g.cascadeRows}`,
2819
+ );
2820
+ }
2821
+ const warnErrors = Object.entries(det.extensionWarnErrors ?? {});
2822
+ if (warnErrors.length) {
2823
+ console.log("\nExtension diagnostics");
2824
+ for (const [k, v] of warnErrors.slice(0, limit))
2825
+ console.log(` ${String(v).padStart(5)} ${k}`);
2826
+ }
2827
+ const deg = det.hookAwait?.census ?? [];
2828
+ if (deg.length) {
2829
+ console.log("\nDegradation ledger census (max count per pid)");
2830
+ for (const row of deg.slice(0, limit))
2831
+ console.log(` pid ${row.pid}: ${row.kinds.join(", ")}`);
2832
+ }
2833
+ const delivery = det.testRunnerStaleVerdicts;
2834
+ if (delivery) {
2835
+ console.log("\nTest-runner delivery");
2836
+ console.log(
2837
+ ` firings=${delivery.firings} stale=${delivery.stale} (${(
2838
+ delivery.staleShare * 100
2839
+ ).toFixed(1)}%)`,
2840
+ );
2841
+ for (const s of delivery.sessions ?? [])
2842
+ console.log(
2843
+ ` ${s.sessionId}: delivered ${s.delivered}/${s.total} staged=${s.staged} eligible=${s.eligible} superseded=${s.superseded}`,
2844
+ );
2845
+ }
2846
+ const knip = det.knip;
2847
+ if (knip && (knip.drift.length || knip.cost.length)) {
2848
+ console.log("\nknip drift and cost");
2849
+ for (const d of knip.drift)
2850
+ console.log(
2851
+ ` pid ${d.pid}: issues ${d.min} → ${d.max} (${d.increments} increments >= 100)`,
2852
+ );
2853
+ for (const c of knip.cost)
2854
+ console.log(
2855
+ ` pid ${c.pid}: ${Math.round(c.perHour)}ms/h, max ${c.maxRow}ms`,
2856
+ );
2857
+ }
2858
+ const extLoads = det.slowExtensionLoad ?? [];
2859
+ if (extLoads.length) {
2860
+ const shortLived = extLoads.filter((l) => l.shortLived).length;
2861
+ console.log("\nExtension loads");
2862
+ console.log(` slow=${extLoads.length} shortLived=${shortLived}`);
2863
+ }
2864
+
1652
2865
  section(
1653
2866
  "Diagnostic rules",
1654
2867
  report.diagnostics.byRule,
@@ -1705,7 +2918,7 @@ function printReport(report) {
1705
2918
  report.latency.testRunnerVerdicts ?? {},
1706
2919
  );
1707
2920
  if (verdictSessions.length) {
1708
- console.log("\nTest-runner stale verdicts per session");
2921
+ console.log("\nTest-runner stale verdicts per session (verdict rows only)");
1709
2922
  for (const [sessionId, summary] of verdictSessions) {
1710
2923
  console.log(
1711
2924
  ` ${sessionId}: ${summary.stale}/${summary.total} stale (${(summary.rate * 100).toFixed(1)}%)`,