pi-lens 4.3.0 → 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 (650) hide show
  1. package/CHANGELOG.md +1064 -0
  2. package/README.md +115 -25
  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 +16456 -8335
  125. package/dist/mcp/analyze-cli.js +186 -232
  126. package/dist/mcp/cli.js +137 -98
  127. package/dist/mcp/server.js +22589 -1573
  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/agent-guide.md +13 -4
  133. package/docs/agent-tools.md +42 -0
  134. package/docs/ast-grep_rules_catalog.md +3 -2
  135. package/docs/configuration.md +20 -1
  136. package/docs/environment-variables.md +52 -2
  137. package/docs/features.md +8 -4
  138. package/docs/globalconfig.md +1 -0
  139. package/docs/io-bridge-v2-migration.md +94 -0
  140. package/docs/lsp-capability-matrix.md +36 -4
  141. package/docs/lsp-idle-eviction.md +124 -0
  142. package/docs/mcp.md +21 -5
  143. package/docs/merge-queue-rollout.md +77 -0
  144. package/docs/pi-lens-fixer.md +156 -88
  145. package/docs/pi-lens-investigator.md +74 -26
  146. package/docs/pi-lens-merge-policy.md +694 -0
  147. package/docs/pi-lens-monitor.md +73 -7
  148. package/docs/pi-lens-retro.md +6 -4
  149. package/docs/pi-lens-reviewer.md +188 -48
  150. package/docs/pi-lens-subagent.md +246 -45
  151. package/docs/pi-lens-warden.md +18 -7
  152. package/docs/public-api-stability.md +55 -0
  153. package/docs/release-qa-baseline.md +84 -17
  154. package/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  155. package/docs/servercapabilities.md +53 -49
  156. package/docs/settings.md +1 -0
  157. package/docs/usage.md +5 -1
  158. package/grammars/tree-sitter-bash.wasm +0 -0
  159. package/grammars/tree-sitter-bash.wasm.json +3 -3
  160. package/package.json +19 -7
  161. package/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  162. package/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  163. package/scripts/analyze-pi-lens-logs.mjs +1275 -62
  164. package/scripts/download-grammars.js +5 -0
  165. package/scripts/grammars.lock.json +6 -1
  166. package/scripts/install-selftest.mjs +33 -34
  167. package/scripts/lib/host-provided-deps.mjs +13 -4
  168. package/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  169. package/dist/clients/actionable-warnings-logger.js +0 -35
  170. package/dist/clients/actionable-warnings.js +0 -1610
  171. package/dist/clients/advisory-provenance.js +0 -593
  172. package/dist/clients/agent-behavior-client.js +0 -147
  173. package/dist/clients/agent-nudge.js +0 -325
  174. package/dist/clients/analysed-root.js +0 -1
  175. package/dist/clients/ast-grep-client.js +0 -731
  176. package/dist/clients/ast-grep-rule-manager.js +0 -146
  177. package/dist/clients/ast-grep-tool-logger.js +0 -118
  178. package/dist/clients/ast-grep-types.js +0 -10
  179. package/dist/clients/ast-grep-yaml-synth.js +0 -154
  180. package/dist/clients/atomic-write-staging.js +0 -75
  181. package/dist/clients/atomic-write.js +0 -189
  182. package/dist/clients/availability-probe-flight.js +0 -12
  183. package/dist/clients/bash-file-access.js +0 -1056
  184. package/dist/clients/biome-client.js +0 -357
  185. package/dist/clients/blocker-freshness.js +0 -837
  186. package/dist/clients/blocker-past-eof.js +0 -129
  187. package/dist/clients/bootstrap.js +0 -569
  188. package/dist/clients/bounded-cache.js +0 -190
  189. package/dist/clients/bounded-pid-file-lock.js +0 -202
  190. package/dist/clients/bounded-telemetry.js +0 -295
  191. package/dist/clients/build-identity.js +0 -95
  192. package/dist/clients/bundled-resource-health.js +0 -113
  193. package/dist/clients/bus-events-logger.js +0 -105
  194. package/dist/clients/bus-publish.js +0 -171
  195. package/dist/clients/cache/rule-cache.js +0 -190
  196. package/dist/clients/cache-manager.js +0 -524
  197. package/dist/clients/cache-observability.js +0 -1313
  198. package/dist/clients/call-graph.js +0 -852
  199. package/dist/clients/cargo-manifest.js +0 -422
  200. package/dist/clients/cascade-budget.js +0 -196
  201. package/dist/clients/cascade-format.js +0 -315
  202. package/dist/clients/cascade-logger.js +0 -31
  203. package/dist/clients/cascade-types.js +0 -9
  204. package/dist/clients/child-unref.js +0 -124
  205. package/dist/clients/code-quality-warnings.js +0 -286
  206. package/dist/clients/codebase-model.js +0 -173
  207. package/dist/clients/collateral-test-role.js +0 -40
  208. package/dist/clients/complexity-client.js +0 -462
  209. package/dist/clients/config-core/deny.js +0 -221
  210. package/dist/clients/config-core/index.js +0 -50
  211. package/dist/clients/config-core/merge.js +0 -357
  212. package/dist/clients/config-core/normalize.js +0 -340
  213. package/dist/clients/config-core/process-spec.js +0 -248
  214. package/dist/clients/config-core/provenance.js +0 -164
  215. package/dist/clients/config-core/records.js +0 -218
  216. package/dist/clients/config-core/resolve.js +0 -125
  217. package/dist/clients/config-core/safe-object.js +0 -78
  218. package/dist/clients/config-core/schema.js +0 -165
  219. package/dist/clients/config-diagnostic-codes.js +0 -285
  220. package/dist/clients/config-locations.js +0 -388
  221. package/dist/clients/config-resolve.js +0 -798
  222. package/dist/clients/config-schema.js +0 -251
  223. package/dist/clients/config-warn.js +0 -407
  224. package/dist/clients/console-guard-install.js +0 -26
  225. package/dist/clients/cooperative-budget.js +0 -42
  226. package/dist/clients/dead-code-client.js +0 -510
  227. package/dist/clients/dead-code-logger.js +0 -30
  228. package/dist/clients/deadline-utils.js +0 -280
  229. package/dist/clients/debug-handles.js +0 -211
  230. package/dist/clients/debug-heap.js +0 -174
  231. package/dist/clients/deferred-lsp-work.js +0 -106
  232. package/dist/clients/degradation-ledger.js +0 -426
  233. package/dist/clients/demoted-finding-render.js +0 -169
  234. package/dist/clients/dependency-checker.js +0 -885
  235. package/dist/clients/deps/ast-grep-napi.js +0 -22
  236. package/dist/clients/deps/js-yaml.js +0 -9
  237. package/dist/clients/deps/minimatch.js +0 -4
  238. package/dist/clients/deps/pi-tui.js +0 -14
  239. package/dist/clients/deps/typebox.js +0 -14
  240. package/dist/clients/deps/vscode-jsonrpc.js +0 -1
  241. package/dist/clients/deps/web-tree-sitter.js +0 -24
  242. package/dist/clients/diagnostic-dispositions.js +0 -630
  243. package/dist/clients/diagnostic-line-freshness.js +0 -305
  244. package/dist/clients/diagnostic-logger.js +0 -73
  245. package/dist/clients/diagnostic-tracker.js +0 -105
  246. package/dist/clients/diagnostics-publish.js +0 -230
  247. package/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  248. package/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  249. package/dist/clients/dispatch/collect-later-tier.js +0 -35
  250. package/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  251. package/dist/clients/dispatch/dispatcher.js +0 -1269
  252. package/dist/clients/dispatch/fact-provider-types.js +0 -1
  253. package/dist/clients/dispatch/fact-rule-runner.js +0 -17
  254. package/dist/clients/dispatch/fact-runner.js +0 -30
  255. package/dist/clients/dispatch/fact-scheduler.js +0 -68
  256. package/dist/clients/dispatch/fact-store.js +0 -377
  257. package/dist/clients/dispatch/facts/comment-facts.js +0 -22
  258. package/dist/clients/dispatch/facts/file-content.js +0 -19
  259. package/dist/clients/dispatch/facts/function-facts.js +0 -348
  260. package/dist/clients/dispatch/facts/import-facts.js +0 -204
  261. package/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  262. package/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  263. package/dist/clients/dispatch/finding-policy.js +0 -339
  264. package/dist/clients/dispatch/indent-detect.js +0 -417
  265. package/dist/clients/dispatch/inline-suppressions.js +0 -89
  266. package/dist/clients/dispatch/integration.js +0 -2377
  267. package/dist/clients/dispatch/lazy.js +0 -11
  268. package/dist/clients/dispatch/pending-runner-findings.js +0 -87
  269. package/dist/clients/dispatch/plan.js +0 -278
  270. package/dist/clients/dispatch/priorities.js +0 -20
  271. package/dist/clients/dispatch/rule-id-normalize.js +0 -50
  272. package/dist/clients/dispatch/rule-ignores.js +0 -69
  273. package/dist/clients/dispatch/rule-policy.js +0 -135
  274. package/dist/clients/dispatch/rules/async-noise.js +0 -40
  275. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  276. package/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  277. package/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  278. package/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  279. package/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  280. package/dist/clients/dispatch/rules/high-complexity.js +0 -72
  281. package/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  282. package/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  283. package/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  284. package/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  285. package/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  286. package/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  287. package/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  288. package/dist/clients/dispatch/runner-context.js +0 -52
  289. package/dist/clients/dispatch/runners/actionlint.js +0 -111
  290. package/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  291. package/dist/clients/dispatch/runners/biome-check.js +0 -296
  292. package/dist/clients/dispatch/runners/cpp-check.js +0 -219
  293. package/dist/clients/dispatch/runners/credo.js +0 -82
  294. package/dist/clients/dispatch/runners/cue-vet.js +0 -416
  295. package/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  296. package/dist/clients/dispatch/runners/detekt.js +0 -176
  297. package/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  298. package/dist/clients/dispatch/runners/elixir-check.js +0 -195
  299. package/dist/clients/dispatch/runners/eslint.js +0 -137
  300. package/dist/clients/dispatch/runners/fact-rules.js +0 -29
  301. package/dist/clients/dispatch/runners/fish-indent.js +0 -69
  302. package/dist/clients/dispatch/runners/gleam-check.js +0 -167
  303. package/dist/clients/dispatch/runners/go-vet.js +0 -104
  304. package/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  305. package/dist/clients/dispatch/runners/hadolint.js +0 -91
  306. package/dist/clients/dispatch/runners/helm-lint.js +0 -171
  307. package/dist/clients/dispatch/runners/helm-render.js +0 -940
  308. package/dist/clients/dispatch/runners/htmlhint.js +0 -117
  309. package/dist/clients/dispatch/runners/index.js +0 -108
  310. package/dist/clients/dispatch/runners/javac.js +0 -99
  311. package/dist/clients/dispatch/runners/ktlint.js +0 -110
  312. package/dist/clients/dispatch/runners/lsp.js +0 -379
  313. package/dist/clients/dispatch/runners/markdownlint.js +0 -167
  314. package/dist/clients/dispatch/runners/mypy.js +0 -102
  315. package/dist/clients/dispatch/runners/oxlint.js +0 -414
  316. package/dist/clients/dispatch/runners/php-lint.js +0 -85
  317. package/dist/clients/dispatch/runners/phpstan.js +0 -120
  318. package/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  319. package/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  320. package/dist/clients/dispatch/runners/pyright.js +0 -169
  321. package/dist/clients/dispatch/runners/rubocop.js +0 -99
  322. package/dist/clients/dispatch/runners/ruff.js +0 -88
  323. package/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  324. package/dist/clients/dispatch/runners/shellcheck.js +0 -173
  325. package/dist/clients/dispatch/runners/shfmt.js +0 -102
  326. package/dist/clients/dispatch/runners/spellcheck.js +0 -128
  327. package/dist/clients/dispatch/runners/spotbugs.js +0 -259
  328. package/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  329. package/dist/clients/dispatch/runners/stylelint.js +0 -166
  330. package/dist/clients/dispatch/runners/swiftlint.js +0 -163
  331. package/dist/clients/dispatch/runners/taplo.js +0 -174
  332. package/dist/clients/dispatch/runners/terragrunt.js +0 -168
  333. package/dist/clients/dispatch/runners/tflint.js +0 -125
  334. package/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  335. package/dist/clients/dispatch/runners/trivy-config.js +0 -253
  336. package/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  337. package/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  338. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  339. package/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  340. package/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  341. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  342. package/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  343. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  344. package/dist/clients/dispatch/runners/utils.js +0 -15
  345. package/dist/clients/dispatch/runners/vale.js +0 -128
  346. package/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  347. package/dist/clients/dispatch/runners/yamllint.js +0 -95
  348. package/dist/clients/dispatch/runners/zig-check.js +0 -79
  349. package/dist/clients/dispatch/suppress-writer.js +0 -79
  350. package/dist/clients/dispatch/tool-profile.js +0 -30
  351. package/dist/clients/dispatch/types.js +0 -21
  352. package/dist/clients/dispatch/utils/format-utils.js +0 -45
  353. package/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  354. package/dist/clients/disposition-logger.js +0 -32
  355. package/dist/clients/disposition-publish.js +0 -124
  356. package/dist/clients/durable-store.js +0 -97
  357. package/dist/clients/effective-config.js +0 -415
  358. package/dist/clients/env-utils.js +0 -83
  359. package/dist/clients/error-class.js +0 -23
  360. package/dist/clients/eval-timestamp.js +0 -17
  361. package/dist/clients/event-loop-hold.js +0 -274
  362. package/dist/clients/event-loop-monitor.js +0 -250
  363. package/dist/clients/extension-log.js +0 -504
  364. package/dist/clients/extension-mode.js +0 -73
  365. package/dist/clients/feature-hints.js +0 -103
  366. package/dist/clients/file-kinds.js +0 -412
  367. package/dist/clients/file-role.js +0 -134
  368. package/dist/clients/file-time.js +0 -160
  369. package/dist/clients/file-utils.js +0 -1032
  370. package/dist/clients/finding-delivery-gate.js +0 -494
  371. package/dist/clients/finding-identity.js +0 -88
  372. package/dist/clients/fix-worklog.js +0 -103
  373. package/dist/clients/format-events-publish.js +0 -343
  374. package/dist/clients/format-service.js +0 -222
  375. package/dist/clients/formatters-lazy.js +0 -9
  376. package/dist/clients/formatters.js +0 -2032
  377. package/dist/clients/freshness-cadence.js +0 -17
  378. package/dist/clients/freshness.js +0 -38
  379. package/dist/clients/generated-artifacts.js +0 -394
  380. package/dist/clients/generation-guard.js +0 -215
  381. package/dist/clients/git-guard.js +0 -1136
  382. package/dist/clients/git-tracked-ignore.js +0 -243
  383. package/dist/clients/gitleaks-client.js +0 -509
  384. package/dist/clients/go-client.js +0 -104
  385. package/dist/clients/govulncheck-client.js +0 -605
  386. package/dist/clients/gradle-ktfmt-style.js +0 -348
  387. package/dist/clients/grammar-source.js +0 -465
  388. package/dist/clients/gzip-stage-write.js +0 -135
  389. package/dist/clients/hashline-anchor.js +0 -424
  390. package/dist/clients/hook-budgets.js +0 -76
  391. package/dist/clients/host-edit-normalize.js +0 -140
  392. package/dist/clients/host-ports.js +0 -39
  393. package/dist/clients/indent-retarget.js +0 -154
  394. package/dist/clients/inline-blocker-dispositions.js +0 -186
  395. package/dist/clients/install-diagnostics.js +0 -156
  396. package/dist/clients/installer/index.js +0 -5416
  397. package/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  398. package/dist/clients/installer/managed-tool-refresh.js +0 -783
  399. package/dist/clients/instance-reaper-state.js +0 -7
  400. package/dist/clients/instance-reaper.js +0 -1137
  401. package/dist/clients/instance-registry-lock.js +0 -145
  402. package/dist/clients/instance-registry.js +0 -773
  403. package/dist/clients/jscpd-client.js +0 -378
  404. package/dist/clients/json-cache-read.js +0 -65
  405. package/dist/clients/knip-client.js +0 -788
  406. package/dist/clients/language-policy.js +0 -252
  407. package/dist/clients/language-profile.js +0 -361
  408. package/dist/clients/language-registry.js +0 -603
  409. package/dist/clients/latency-logger.js +0 -727
  410. package/dist/clients/lazy-import.js +0 -21
  411. package/dist/clients/ledger-bounds.js +0 -54
  412. package/dist/clients/lens-config.js +0 -404
  413. package/dist/clients/lens-engine.js +0 -358
  414. package/dist/clients/lens-events.js +0 -125
  415. package/dist/clients/lens-flag-registry.js +0 -467
  416. package/dist/clients/lens-map.js +0 -1156
  417. package/dist/clients/live-bus-emitter.js +0 -74
  418. package/dist/clients/log-cleanup.js +0 -222
  419. package/dist/clients/lsp/aggregation.js +0 -178
  420. package/dist/clients/lsp/cascade-tier.js +0 -421
  421. package/dist/clients/lsp/client.js +0 -4593
  422. package/dist/clients/lsp/config.js +0 -691
  423. package/dist/clients/lsp/content-limits.js +0 -41
  424. package/dist/clients/lsp/diagnostic-binding.js +0 -225
  425. package/dist/clients/lsp/document-drift.js +0 -467
  426. package/dist/clients/lsp/edits.js +0 -1219
  427. package/dist/clients/lsp/index.js +0 -8305
  428. package/dist/clients/lsp/inferred-project.js +0 -216
  429. package/dist/clients/lsp/jvm-runtime.js +0 -298
  430. package/dist/clients/lsp/language.js +0 -63
  431. package/dist/clients/lsp/launch.js +0 -746
  432. package/dist/clients/lsp/lombok.js +0 -158
  433. package/dist/clients/lsp/path-utils.js +0 -5
  434. package/dist/clients/lsp/pending-aux-coverage.js +0 -251
  435. package/dist/clients/lsp/position-encoding.js +0 -61
  436. package/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  437. package/dist/clients/lsp/server.js +0 -3290
  438. package/dist/clients/lsp/session-roots.js +0 -192
  439. package/dist/clients/lsp/spawn-history.js +0 -17
  440. package/dist/clients/lsp/sync-kind.js +0 -56
  441. package/dist/clients/lsp/tsserver-sync.js +0 -332
  442. package/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  443. package/dist/clients/lsp/wait-policy/classification.js +0 -85
  444. package/dist/clients/lsp/wait-policy/index.js +0 -10
  445. package/dist/clients/lsp/wait-policy/strategies.js +0 -392
  446. package/dist/clients/lsp/watch-queue.js +0 -61
  447. package/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  448. package/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  449. package/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  450. package/dist/clients/lsp-budget.js +0 -186
  451. package/dist/clients/lsp-document-symbols.js +0 -110
  452. package/dist/clients/lsp-lazy.js +0 -9
  453. package/dist/clients/lsp-mutation.js +0 -396
  454. package/dist/clients/lsp-status.js +0 -59
  455. package/dist/clients/map-with-concurrency.js +0 -40
  456. package/dist/clients/mcp/analyze.js +0 -420
  457. package/dist/clients/mcp/host-shim.js +0 -31
  458. package/dist/clients/mcp/ipc.js +0 -523
  459. package/dist/clients/mcp/review.js +0 -197
  460. package/dist/clients/mcp/session.js +0 -412
  461. package/dist/clients/memory-sampler.js +0 -208
  462. package/dist/clients/message-end-attribution.js +0 -30
  463. package/dist/clients/metrics-client.js +0 -107
  464. package/dist/clients/metrics-history.js +0 -327
  465. package/dist/clients/middle-man-analysis.js +0 -263
  466. package/dist/clients/model-provider.js +0 -53
  467. package/dist/clients/module-report-lsp.js +0 -189
  468. package/dist/clients/module-report.js +0 -2150
  469. package/dist/clients/mutating-tool.js +0 -651
  470. package/dist/clients/mutation-attribution.js +0 -368
  471. package/dist/clients/mutation-bridge.js +0 -240
  472. package/dist/clients/ndjson-logger.js +0 -734
  473. package/dist/clients/observed-mutation-sources.js +0 -101
  474. package/dist/clients/observed-mutation.js +0 -1253
  475. package/dist/clients/oldtext-autopatch.js +0 -128
  476. package/dist/clients/opaque-mutation-scan.js +0 -496
  477. package/dist/clients/opengrep-client.js +0 -333
  478. package/dist/clients/opengrep-config.js +0 -76
  479. package/dist/clients/package-manager.js +0 -607
  480. package/dist/clients/package-root.js +0 -38
  481. package/dist/clients/partial-edit-apply.js +0 -439
  482. package/dist/clients/path-attribution-telemetry.js +0 -43
  483. package/dist/clients/path-keyed-map.js +0 -108
  484. package/dist/clients/path-utils.js +0 -1244
  485. package/dist/clients/performance-report.js +0 -331
  486. package/dist/clients/persist-debounce.js +0 -70
  487. package/dist/clients/persistent-reverify.js +0 -271
  488. package/dist/clients/php-cs-fixer-config.js +0 -119
  489. package/dist/clients/pipeline.js +0 -1480
  490. package/dist/clients/post-autofix-notice.js +0 -37
  491. package/dist/clients/print-mode.js +0 -21
  492. package/dist/clients/probe-home-state.js +0 -228
  493. package/dist/clients/process-bridge.js +0 -66
  494. package/dist/clients/process-singletons.js +0 -210
  495. package/dist/clients/process-snapshot.js +0 -68
  496. package/dist/clients/project-changes.js +0 -181
  497. package/dist/clients/project-conventions.js +0 -171
  498. package/dist/clients/project-diagnostics/cache.js +0 -137
  499. package/dist/clients/project-diagnostics/extractors.js +0 -109
  500. package/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  501. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  502. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  503. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  504. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  505. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  506. package/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  507. package/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  508. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  509. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  510. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  511. package/dist/clients/project-diagnostics/scanner.js +0 -520
  512. package/dist/clients/project-diagnostics/types.js +0 -1
  513. package/dist/clients/project-lens-config.js +0 -807
  514. package/dist/clients/project-report.js +0 -701
  515. package/dist/clients/project-scale.js +0 -237
  516. package/dist/clients/project-scan-policy.js +0 -11
  517. package/dist/clients/project-snapshot-fingerprint.js +0 -48
  518. package/dist/clients/project-snapshot-persist-worker.js +0 -20
  519. package/dist/clients/project-snapshot.js +0 -1653
  520. package/dist/clients/project-trust.js +0 -181
  521. package/dist/clients/python-environment.js +0 -217
  522. package/dist/clients/python-provenance.js +0 -639
  523. package/dist/clients/quiet-window-config.js +0 -47
  524. package/dist/clients/quiet-window.js +0 -236
  525. package/dist/clients/read-bridge.js +0 -139
  526. package/dist/clients/read-expansion.js +0 -266
  527. package/dist/clients/read-guard-logger.js +0 -251
  528. package/dist/clients/read-guard-tool-lines.js +0 -1306
  529. package/dist/clients/read-guard.js +0 -1561
  530. package/dist/clients/recent-touches.js +0 -240
  531. package/dist/clients/redact/secrets.js +0 -449
  532. package/dist/clients/resource-sampler.js +0 -628
  533. package/dist/clients/reverse-deps.js +0 -270
  534. package/dist/clients/review-graph/builder.js +0 -5126
  535. package/dist/clients/review-graph/format.js +0 -33
  536. package/dist/clients/review-graph/git-identity.js +0 -163
  537. package/dist/clients/review-graph/import-resolvers.js +0 -481
  538. package/dist/clients/review-graph/persist-worker.js +0 -10
  539. package/dist/clients/review-graph/query.js +0 -244
  540. package/dist/clients/review-graph/revision-drift.js +0 -21
  541. package/dist/clients/review-graph/service.js +0 -96
  542. package/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  543. package/dist/clients/review-graph/symbol-id.js +0 -103
  544. package/dist/clients/review-graph/tsconfig-paths.js +0 -299
  545. package/dist/clients/review-graph/types.js +0 -1
  546. package/dist/clients/review-graph/workspace-modules.js +0 -449
  547. package/dist/clients/review-graph-logger.js +0 -81
  548. package/dist/clients/ruff-client.js +0 -179
  549. package/dist/clients/rules-scanner.js +0 -131
  550. package/dist/clients/run-duration.js +0 -55
  551. package/dist/clients/runtime-agent-end.js +0 -746
  552. package/dist/clients/runtime-config.js +0 -77
  553. package/dist/clients/runtime-context.js +0 -211
  554. package/dist/clients/runtime-coordinator.js +0 -1324
  555. package/dist/clients/runtime-session.js +0 -2485
  556. package/dist/clients/runtime-tool-call.js +0 -1295
  557. package/dist/clients/runtime-tool-result.js +0 -2049
  558. package/dist/clients/runtime-turn.js +0 -3767
  559. package/dist/clients/rust-client.js +0 -97
  560. package/dist/clients/safe-spawn.js +0 -2357
  561. package/dist/clients/sanitize.js +0 -286
  562. package/dist/clients/scan-utils.js +0 -25
  563. package/dist/clients/scratch-tree-policy.js +0 -165
  564. package/dist/clients/search-read-registration.js +0 -94
  565. package/dist/clients/secret-findings.js +0 -114
  566. package/dist/clients/security-scan-client.js +0 -352
  567. package/dist/clients/session-event-guard.js +0 -297
  568. package/dist/clients/session-lifecycle.js +0 -484
  569. package/dist/clients/session-start-observability.js +0 -94
  570. package/dist/clients/session-state-store.js +0 -101
  571. package/dist/clients/session-summary.js +0 -15
  572. package/dist/clients/sessionstart-logger.js +0 -27
  573. package/dist/clients/sg-runner.js +0 -876
  574. package/dist/clients/sgconfig.js +0 -347
  575. package/dist/clients/shared-checkout-guard.js +0 -338
  576. package/dist/clients/single-flight.js +0 -158
  577. package/dist/clients/situational-tool-telemetry.js +0 -108
  578. package/dist/clients/skills-resolver.js +0 -141
  579. package/dist/clients/slow-fs.js +0 -138
  580. package/dist/clients/smells-rollup.js +0 -231
  581. package/dist/clients/source-filter.js +0 -596
  582. package/dist/clients/source-walker.js +0 -211
  583. package/dist/clients/spawn-output-cap.js +0 -120
  584. package/dist/clients/spawn-timeout-cooldown.js +0 -110
  585. package/dist/clients/stale-marker.js +0 -17
  586. package/dist/clients/startup-scan.js +0 -400
  587. package/dist/clients/startup-timing.js +0 -130
  588. package/dist/clients/string-utils.js +0 -33
  589. package/dist/clients/subagent-mode.js +0 -111
  590. package/dist/clients/symbol-containment.js +0 -24
  591. package/dist/clients/symbol-types.js +0 -5
  592. package/dist/clients/test-runner-client.js +0 -2507
  593. package/dist/clients/test-runner-delivery.js +0 -287
  594. package/dist/clients/todo-scanner.js +0 -229
  595. package/dist/clients/tool-agreement.js +0 -423
  596. package/dist/clients/tool-config.js +0 -249
  597. package/dist/clients/tool-cwd.js +0 -270
  598. package/dist/clients/tool-definition.js +0 -47
  599. package/dist/clients/tool-event.js +0 -105
  600. package/dist/clients/tool-policy.js +0 -2755
  601. package/dist/clients/tool-probe.js +0 -53
  602. package/dist/clients/tool-render.js +0 -156
  603. package/dist/clients/tool-set-policy.js +0 -118
  604. package/dist/clients/tree-sitter-cache.js +0 -364
  605. package/dist/clients/tree-sitter-client.js +0 -4195
  606. package/dist/clients/tree-sitter-logger.js +0 -100
  607. package/dist/clients/tree-sitter-navigator.js +0 -269
  608. package/dist/clients/tree-sitter-query-loader.js +0 -569
  609. package/dist/clients/tree-sitter-shared.js +0 -158
  610. package/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  611. package/dist/clients/trivy-client.js +0 -467
  612. package/dist/clients/tui-fit.js +0 -50
  613. package/dist/clients/turn-context.js +0 -52
  614. package/dist/clients/turn-end/lane.js +0 -51
  615. package/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  616. package/dist/clients/turn-end/lanes/secrets.js +0 -204
  617. package/dist/clients/turn-summary-render.js +0 -72
  618. package/dist/clients/turn-summary.js +0 -132
  619. package/dist/clients/typos-config.js +0 -29
  620. package/dist/clients/user-notify.js +0 -51
  621. package/dist/clients/vanished-instance-marker.js +0 -57
  622. package/dist/clients/warm-attach.js +0 -305
  623. package/dist/clients/widget-state.js +0 -1808
  624. package/dist/clients/word-index-logger.js +0 -49
  625. package/dist/clients/word-index-store.js +0 -459
  626. package/dist/clients/word-index.js +0 -2080
  627. package/dist/clients/workspace-topology.js +0 -443
  628. package/dist/clients/write-ordering-guard.js +0 -65
  629. package/dist/clients/zizmor-config.js +0 -340
  630. package/dist/i18n.js +0 -69
  631. package/dist/mcp/build-staleness.js +0 -124
  632. package/dist/scripts/lib/process-scan.mjs +0 -583
  633. package/dist/scripts/lib/skills-predicate.mjs +0 -129
  634. package/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  635. package/dist/tools/activate-tools.js +0 -87
  636. package/dist/tools/ast-grep-outline.js +0 -137
  637. package/dist/tools/ast-grep-replace.js +0 -244
  638. package/dist/tools/ast-grep-search.js +0 -762
  639. package/dist/tools/effective-config.js +0 -90
  640. package/dist/tools/lens-diagnostic-mark.js +0 -375
  641. package/dist/tools/lens-diagnostics.js +0 -2571
  642. package/dist/tools/lsp-diagnostics.js +0 -1745
  643. package/dist/tools/lsp-navigation.js +0 -1367
  644. package/dist/tools/lsp-structured-output.js +0 -294
  645. package/dist/tools/module-report.js +0 -382
  646. package/dist/tools/project-report.js +0 -86
  647. package/dist/tools/render-compact.js +0 -374
  648. package/dist/tools/scan-progress.js +0 -53
  649. package/dist/tools/shared.js +0 -29
  650. 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)}%)`,