@mrclrchtr/supi-code-intelligence 4.10.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +4 -3
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  6. package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  8. package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  9. package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  10. package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  11. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
  14. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  16. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  17. package/node_modules/@mrclrchtr/supi-lsp/README.md +111 -5
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
  30. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  32. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  34. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +22 -1
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +245 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +171 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +413 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +15 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +690 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +97 -16
  45. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  46. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +448 -298
  47. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +70 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +231 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +529 -119
  51. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
  52. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +12 -3
  53. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
  54. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  55. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
  59. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +42 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
  62. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  63. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +17 -7
  64. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +350 -26
  65. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  66. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +586 -96
  67. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  68. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  69. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  70. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  71. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  72. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  73. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  74. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +42 -0
  75. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +92 -0
  76. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +165 -118
  77. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  78. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +17 -7
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +6 -3
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  127. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  128. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  129. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  130. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  131. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  132. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  133. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  134. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  135. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  136. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  137. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  138. package/package.json +5 -5
  139. package/src/analysis/health/diagnostics.ts +221 -77
  140. package/src/analysis/health/file-scope.ts +184 -0
  141. package/src/analysis/health/recovery.ts +14 -3
  142. package/src/analysis/provider.ts +58 -0
  143. package/src/analysis/readiness.ts +34 -9
  144. package/src/analysis/refactor/apply.ts +45 -1
  145. package/src/analysis/refactor/mutation-authority.ts +143 -0
  146. package/src/analysis/relations/callees.ts +9 -1
  147. package/src/analysis/relations/types.ts +5 -2
  148. package/src/analysis/search/ast-scan-timing.ts +9 -2
  149. package/src/analysis/search/ast-scan.ts +5 -2
  150. package/src/analysis/search/deadline.ts +2 -2
  151. package/src/analysis/search/pattern-analysis.ts +54 -33
  152. package/src/analysis/search/pattern.ts +18 -3
  153. package/src/analysis/target/anchored.ts +20 -11
  154. package/src/analysis/target/file.ts +18 -11
  155. package/src/analysis/target/identity.ts +19 -3
  156. package/src/analysis/target/symbol.ts +36 -15
  157. package/src/api.ts +4 -0
  158. package/src/config.ts +35 -1
  159. package/src/diagnostics/evidence.ts +51 -0
  160. package/src/extension.ts +57 -14
  161. package/src/overview/overview-data.ts +9 -2
  162. package/src/overview/overview.ts +49 -30
  163. package/src/overview/types.ts +2 -0
  164. package/src/session/capability-adapter.ts +12 -3
  165. package/src/session/find-types.ts +2 -2
  166. package/src/session/find-workflow.ts +20 -9
  167. package/src/session/graph/collect.ts +18 -2
  168. package/src/session/graph-workflow.ts +19 -12
  169. package/src/session/health-refresh.ts +175 -0
  170. package/src/session/health-types.ts +43 -15
  171. package/src/session/health-workflow.ts +69 -54
  172. package/src/session/input/workflows.ts +2 -2
  173. package/src/session/inspect/collect.ts +20 -3
  174. package/src/session/inspect-workflow.ts +12 -6
  175. package/src/session/orientation/collect.ts +12 -3
  176. package/src/session/orientation/context-facts.ts +3 -1
  177. package/src/session/orientation/context-sections.ts +20 -7
  178. package/src/session/orientation/gather.ts +17 -11
  179. package/src/session/orientation-types.ts +4 -1
  180. package/src/session/orientation-workflow.ts +23 -10
  181. package/src/session/refactor-plans.ts +2 -0
  182. package/src/session/refactor-workflow.ts +61 -15
  183. package/src/session/session.ts +29 -2
  184. package/src/session/target-workflow.ts +54 -23
  185. package/src/session/workflow-control.ts +7 -3
  186. package/src/substrate/lsp/lifecycle.ts +26 -46
  187. package/src/substrate/lsp/maintenance.ts +284 -28
  188. package/src/substrate/lsp/recovery.ts +11 -5
  189. package/src/substrate/lsp/state.ts +2 -2
  190. package/src/substrate/workspace-provider-host.ts +25 -3
  191. package/src/tool/{find → code_find}/execute.ts +2 -16
  192. package/src/tool/code_find/guidance.ts +8 -0
  193. package/src/tool/{find → code_find}/render.ts +21 -4
  194. package/src/tool/{result/find.ts → code_find/result.ts} +78 -9
  195. package/src/tool/code_find/spec.ts +37 -0
  196. package/src/tool/code_find/tui.ts +41 -0
  197. package/src/tool/code_graph/execute.ts +24 -0
  198. package/src/tool/code_graph/guidance.ts +6 -0
  199. package/src/tool/{graph → code_graph}/markdown.ts +1 -1
  200. package/src/tool/{result/graph.ts → code_graph/result.ts} +111 -3
  201. package/src/tool/code_graph/spec.ts +39 -0
  202. package/src/tool/code_graph/tui.ts +117 -0
  203. package/src/tool/{health → code_health}/execute.ts +2 -13
  204. package/src/tool/code_health/guidance.ts +8 -0
  205. package/src/tool/{health → code_health}/markdown.ts +119 -27
  206. package/src/tool/code_health/refresh-status.ts +105 -0
  207. package/src/tool/{result/health.ts → code_health/result.ts} +80 -3
  208. package/src/tool/code_health/spec.ts +41 -0
  209. package/src/tool/{health → code_health}/tui.ts +95 -98
  210. package/src/tool/{inspect → code_inspect}/execute.ts +2 -16
  211. package/src/tool/code_inspect/guidance.ts +6 -0
  212. package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
  213. package/src/tool/{result/inspect.ts → code_inspect/result.ts} +143 -2
  214. package/src/tool/code_inspect/spec.ts +25 -0
  215. package/src/tool/code_inspect/tui.ts +44 -0
  216. package/src/tool/code_orientation/execute.ts +25 -0
  217. package/src/tool/code_orientation/guidance.ts +9 -0
  218. package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
  219. package/src/tool/code_orientation/result.ts +248 -0
  220. package/src/tool/code_orientation/spec.ts +25 -0
  221. package/src/tool/{orientation → code_orientation}/tui.ts +51 -35
  222. package/src/tool/code_refactor_apply/execute.ts +17 -0
  223. package/src/tool/code_refactor_apply/guidance.ts +5 -0
  224. package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +8 -11
  225. package/src/tool/code_refactor_apply/spec.ts +26 -0
  226. package/src/tool/code_refactor_apply/tui.ts +73 -0
  227. package/src/tool/code_refactor_plan/execute.ts +27 -0
  228. package/src/tool/code_refactor_plan/guidance.ts +8 -0
  229. package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +13 -22
  230. package/src/tool/code_refactor_plan/spec.ts +25 -0
  231. package/src/tool/code_refactor_plan/tui.ts +54 -0
  232. package/src/tool/code_resolve/execute.ts +19 -0
  233. package/src/tool/code_resolve/guidance.ts +8 -0
  234. package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
  235. package/src/tool/{result/resolve.ts → code_resolve/result.ts} +79 -29
  236. package/src/tool/code_resolve/spec.ts +25 -0
  237. package/src/tool/code_resolve/tui.ts +60 -0
  238. package/src/tool/guidance.ts +71 -52
  239. package/src/tool/infra/truncate.ts +40 -0
  240. package/src/tool/infra/workflow-control.ts +8 -0
  241. package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
  242. package/src/tool/register.ts +105 -88
  243. package/src/tool/result/display.ts +107 -0
  244. package/src/tool/result/errors.ts +20 -17
  245. package/src/tool/result/refactor.ts +37 -8
  246. package/src/tool/result/types.ts +20 -0
  247. package/src/tool/schemas.ts +26 -157
  248. package/src/tool/specs.ts +40 -126
  249. package/src/types/execution.ts +18 -5
  250. package/src/types/index.ts +8 -1
  251. package/src/ui/status-command.ts +5 -3
  252. package/src/ui/status-overlay.ts +4 -0
  253. package/src/ui/tui/common.ts +108 -10
  254. package/src/ui/tui/display.ts +48 -0
  255. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -351
  256. package/src/tool/find/tui.ts +0 -39
  257. package/src/tool/graph/execute.ts +0 -62
  258. package/src/tool/graph/tui.ts +0 -104
  259. package/src/tool/inspect/tui.ts +0 -36
  260. package/src/tool/orientation/execute.ts +0 -62
  261. package/src/tool/refactor-apply/tui.ts +0 -55
  262. package/src/tool/refactor-plan/tui.ts +0 -41
  263. package/src/tool/resolve/execute.ts +0 -36
  264. package/src/tool/resolve/tui.ts +0 -42
  265. package/src/tool/result/orientation.ts +0 -132
  266. /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
  267. /package/src/tool/{find → code_find}/markdown.ts +0 -0
  268. /package/src/tool/{find → code_find}/modes.ts +0 -0
  269. /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
@@ -2,10 +2,27 @@
2
2
  // biome-ignore-all lint/style/noExcessiveLinesPerFile: LspManager stays cohesive; recovery and sync helpers are split into manager-*.ts modules.
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
- import { type CodeQueryResult, unavailableCodeQuery } from "@mrclrchtr/supi-code-runtime/api";
5
+ import {
6
+ type CodeQueryResult,
7
+ type CodeRequestControl,
8
+ isCodeRequestInterruption,
9
+ throwIfCodeRequestInterrupted,
10
+ unavailableCodeQuery,
11
+ } from "@mrclrchtr/supi-code-runtime/api";
6
12
  import * as projectRoots from "@mrclrchtr/supi-core/project";
7
- import { LspClient } from "../client/client.ts";
13
+ import * as ts from "typescript";
14
+ import {
15
+ LspClient,
16
+ type LspClientLifecycleTransitionKind,
17
+ RECOVERY_CLIENT_STARTUP_BOUND_MS,
18
+ withTimeout,
19
+ } from "../client/client.ts";
20
+ import type { ClientDiagnosticSnapshot } from "../client/client-document-state.ts";
21
+
22
+ export { RECOVERY_CLIENT_STARTUP_BOUND_MS } from "../client/client.ts";
23
+
8
24
  import { getServerForFile } from "../config/config.ts";
25
+ import { getFileScopeDecision } from "../config/tsconfig-scope.ts";
9
26
  import type {
10
27
  DetectedProjectServer,
11
28
  Diagnostic,
@@ -22,6 +39,17 @@ import {
22
39
  createOutstandingDiagnosticSummary,
23
40
  relativeFilePathFromUri,
24
41
  } from "../diagnostics/diagnostic-summary.ts";
42
+ import {
43
+ type DiagnosticEvidenceDocument,
44
+ type DiagnosticEvidenceSummary,
45
+ summarizeDiagnosticEvidence,
46
+ } from "../diagnostics/evidence.ts";
47
+ import { raceRequestControl } from "../session/readiness.ts";
48
+ import type {
49
+ ScopeDecisionEntry,
50
+ ScopeDecisionSummary,
51
+ WorkspaceDiagnosticReport,
52
+ } from "../session/runtime-diagnostics.ts";
25
53
  import {
26
54
  displayRelativeFilePath,
27
55
  formatCoverageSummaryText,
@@ -30,7 +58,7 @@ import {
30
58
  normalizeRelevantPaths,
31
59
  shouldIgnoreLspPath,
32
60
  } from "../summary.ts";
33
- import { commandExists, resolveSessionPath } from "../utils.ts";
61
+ import { commandExists, resolveSessionPath, uriToFile } from "../utils.ts";
34
62
  import {
35
63
  closeFileAcrossClients,
36
64
  pruneMissingFilesFromClients,
@@ -55,7 +83,10 @@ import type {
55
83
  OutstandingDiagnosticSummaryEntry,
56
84
  ServerStatus,
57
85
  } from "./manager-types.ts";
58
- import { recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl } from "./manager-workspace-recovery.ts";
86
+ import {
87
+ recoverWorkspaceDiagnostics as recoverWorkspaceDiagnosticsImpl,
88
+ type WorkspaceRecoveryResult,
89
+ } from "./manager-workspace-recovery.ts";
59
90
  import {
60
91
  collectWorkspaceSymbols,
61
92
  findWorkspaceSymbolWarmTargets,
@@ -66,6 +97,35 @@ import {
66
97
 
67
98
  type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
68
99
 
100
+ /** Maximum tracked-file entries retained in one scope-decision telemetry summary. */
101
+ const SCOPE_DECISION_MAX_ENTRIES = 24;
102
+
103
+ interface ScopeDecisionAccumulator {
104
+ entries: ScopeDecisionEntry[];
105
+ counts: ScopeDecisionSummary["counts"];
106
+ basisCounts: Record<string, number>;
107
+ }
108
+
109
+ function accumulateScopeDecision(
110
+ accumulator: ScopeDecisionAccumulator,
111
+ file: string,
112
+ decision: ReturnType<typeof getFileScopeDecision>,
113
+ ): void {
114
+ const statusKey =
115
+ decision.status === "no-config"
116
+ ? "noConfig"
117
+ : decision.status === "out-of-tree"
118
+ ? "outOfTree"
119
+ : decision.status;
120
+ accumulator.counts[statusKey]++;
121
+ if (decision.basis) {
122
+ accumulator.basisCounts[decision.basis] = (accumulator.basisCounts[decision.basis] ?? 0) + 1;
123
+ }
124
+ if (accumulator.entries.length < SCOPE_DECISION_MAX_ENTRIES) {
125
+ accumulator.entries.push({ file, status: decision.status, basis: decision.basis });
126
+ }
127
+ }
128
+
69
129
  type FileRoute = {
70
130
  serverName: string;
71
131
  serverConfig: ServerConfig;
@@ -73,6 +133,24 @@ type FileRoute = {
73
133
  key: string;
74
134
  };
75
135
 
136
+ type ClientRestartResult = {
137
+ key: string;
138
+ serverName: string;
139
+ files: string[];
140
+ restarted: boolean;
141
+ };
142
+
143
+ export type ManagerLifecycleTransitionKind = LspClientLifecycleTransitionKind | "recovery";
144
+
145
+ /** Package-internal aggregate lifecycle snapshot for the runtime controller. */
146
+ export interface ManagerLifecycleTransition {
147
+ readonly kind: ManagerLifecycleTransitionKind;
148
+ readonly semanticReady: boolean;
149
+ readonly projectServers: readonly ProjectServerInfo[];
150
+ }
151
+
152
+ type ManagerLifecycleListener = (transition: ManagerLifecycleTransition) => void;
153
+
76
154
  // ── LspManager ────────────────────────────────────────────────────────
77
155
  export class LspManager {
78
156
  /** Active clients keyed by "serverName:root" */
@@ -83,10 +161,16 @@ export class LspManager {
83
161
  private commandAvailability = new Map<string, boolean>();
84
162
  /** Guards against concurrent client creation for the same server:root key */
85
163
  private pendingStarts = new Map<string, Promise<LspClient | null>>();
164
+ /** Monotonic ownership generation for each replaceable client route. */
165
+ private clientGenerations = new Map<string, number>();
86
166
  /** Preferred project roots discovered by proactive scan or lazy startup */
87
167
  private knownRoots = new Map<string, string[]>();
88
168
  /** User-configured gitignore-style exclude patterns */
89
169
  private excludePatterns: string[] = [];
170
+ /** Monotonic workspace invalidation generation advanced by each invalidation event. */
171
+ private invalidationEpoch = 0;
172
+ /** Invalidation generation in which each route last received a recovery restart. */
173
+ private recoveryRestartEpochs = new Map<string, number>();
90
174
  /** Project roots already warmed for workspace-symbol queries. */
91
175
  private warmedWorkspaceSymbolProjects = new Set<string>();
92
176
  /** Project roots whose semantic state was warmed with a readiness probe. */
@@ -96,6 +180,7 @@ export class LspManager {
96
180
  constructor(
97
181
  private readonly config: LspConfig,
98
182
  private readonly cwd: string,
183
+ private readonly onLifecycleTransition?: ManagerLifecycleListener,
99
184
  ) {}
100
185
  getCwd(): string {
101
186
  return this.cwd;
@@ -152,6 +237,15 @@ export class LspManager {
152
237
 
153
238
  return reason;
154
239
  }
240
+
241
+ /** Return the open document version without starting a server. */
242
+ getOpenDocumentVersion(filePath: string): number | null {
243
+ const resolvedPath = resolveSessionPath(this.cwd, filePath);
244
+ return (
245
+ this.getExistingClientForFile(resolvedPath)?.getOpenDocumentVersion(resolvedPath) ?? null
246
+ );
247
+ }
248
+
155
249
  /** Get or create an LSP client for the given file. */
156
250
  async getClientForFile(filePath: string): Promise<LspClient | null> {
157
251
  const route = this.resolveFileRoute(filePath);
@@ -194,6 +288,53 @@ export class LspManager {
194
288
  }
195
289
  }
196
290
 
291
+ private createClient(
292
+ serverName: string,
293
+ serverConfig: ServerConfig,
294
+ root: string,
295
+ key: string,
296
+ ): LspClient {
297
+ const generation = (this.clientGenerations.get(key) ?? 0) + 1;
298
+ this.clientGenerations.set(key, generation);
299
+ let client: LspClient;
300
+ client = new LspClient(
301
+ serverName,
302
+ serverConfig,
303
+ root,
304
+ (kind) => {
305
+ this.handleClientLifecycle(key, generation, client, kind);
306
+ },
307
+ this.cwd,
308
+ );
309
+ return client;
310
+ }
311
+
312
+ private handleClientLifecycle(
313
+ key: string,
314
+ generation: number,
315
+ client: LspClient,
316
+ kind: LspClientLifecycleTransitionKind,
317
+ ): void {
318
+ if (this.clientGenerations.get(key) !== generation) return;
319
+ if (this.clients.get(key) !== client) return;
320
+ const aggregateKind =
321
+ kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
322
+ this.publishLifecycle(aggregateKind);
323
+ }
324
+
325
+ private publishLifecycle(kind: ManagerLifecycleTransitionKind): void {
326
+ if (!this.onLifecycleTransition) return;
327
+ const projectServers = this.getKnownProjectServers([]);
328
+ const semanticReady = projectServers.some(
329
+ (server) => server.status === "running" && server.ready,
330
+ );
331
+ try {
332
+ this.onLifecycleTransition({ kind, semanticReady, projectServers });
333
+ } catch {
334
+ // Runtime lifecycle consumers must not alter manager behavior.
335
+ }
336
+ }
337
+
197
338
  /**
198
339
  * Perform the actual server start — extracted so the public method can
199
340
  * deduplicate via pendingStarts without wrapping the entire body.
@@ -211,7 +352,7 @@ export class LspManager {
211
352
  }
212
353
 
213
354
  // Spawn new client
214
- const client = new LspClient(serverName, serverConfig, root);
355
+ const client = this.createClient(serverName, serverConfig, root, key);
215
356
  this.clearWarmedWorkspaceSymbolProjects(serverName, root);
216
357
  this.clearWarmedSemanticProjects(serverName, root);
217
358
  this.clearPendingWarmProbes(serverName, root);
@@ -247,33 +388,51 @@ export class LspManager {
247
388
  return route ? (this.clients.get(route.key) ?? null) : null;
248
389
  }
249
390
 
250
- /** Restart the clients that own the supplied file paths, if any are active. */
251
- async restartClientsForFiles(filePaths: string[]): Promise<string[]> {
252
- const restarted: string[] = [];
391
+ /**
392
+ * Restart the clients that own the supplied file paths, if any are active.
393
+ *
394
+ * Each route restarts at most once per invalidation generation. Pass
395
+ * `pushOnly` to restrict restarts to clients without pull diagnostics.
396
+ * The loop observes request cancellation between route restarts.
397
+ */
398
+ async restartClientsForFiles(
399
+ filePaths: string[],
400
+ options?: { pushOnly?: boolean; control?: CodeRequestControl },
401
+ ): Promise<ClientRestartResult[]> {
402
+ const restarted: ClientRestartResult[] = [];
253
403
  const seen = new Set<string>();
254
404
 
255
405
  for (const filePath of filePaths) {
256
406
  const client = this.getExistingClientForFile(filePath);
257
407
  if (!client) continue;
408
+ if (options?.pushOnly && client.hasDiagnosticProvider) continue;
258
409
 
259
410
  const key = clientKey(client.name, client.root);
260
411
  if (seen.has(key)) continue;
261
412
  seen.add(key);
413
+ if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) continue;
262
414
 
263
- if (await this.restartClient(client)) {
264
- restarted.push(key);
265
- }
415
+ // Observe cancellation between route restarts so a cancelled pass does
416
+ // not keep restarting further clients.
417
+ throwIfCodeRequestInterrupted(options?.control);
418
+ const result = await this.restartClient(client);
419
+ this.recoveryRestartEpochs.set(key, this.invalidationEpoch);
420
+ restarted.push({ key, serverName: client.name, ...result });
266
421
  }
267
422
 
268
423
  return restarted;
269
424
  }
270
425
 
271
- private async restartClient(client: LspClient): Promise<boolean> {
426
+ private async restartClient(client: LspClient): Promise<{ files: string[]; restarted: boolean }> {
272
427
  const key = clientKey(client.name, client.root);
273
428
  const serverConfig = this.config.servers[client.name];
274
- if (!serverConfig) return false;
429
+ if (!serverConfig) return { files: [], restarted: false };
275
430
 
276
431
  const openFiles = client.openFiles;
432
+ const trackedDiagnosticFiles = client
433
+ .getDiagnosticSnapshot()
434
+ .documents.map((document) => uriToFile(document.uri));
435
+ const files = [...new Set([...openFiles, ...trackedDiagnosticFiles])];
277
436
  try {
278
437
  await client.shutdown();
279
438
  } catch {
@@ -286,25 +445,37 @@ export class LspManager {
286
445
  this.clearWarmedSemanticProjects(client.name, client.root);
287
446
  this.clearPendingWarmProbes(client.name, client.root);
288
447
 
289
- const replacement = new LspClient(client.name, serverConfig, client.root);
448
+ const replacement = this.createClient(client.name, serverConfig, client.root, key);
290
449
  this.clients.set(key, replacement);
291
450
  rememberKnownRoot(this.knownRoots, client.name, client.root);
292
451
 
293
452
  try {
294
- await replacement.start();
295
- for (const filePath of openFiles) {
296
- if (!fs.existsSync(filePath)) continue;
453
+ await withTimeout(
454
+ replacement.start(),
455
+ RECOVERY_CLIENT_STARTUP_BOUND_MS,
456
+ "replacement client startup bound exceeded",
457
+ );
458
+ const failedFiles: string[] = [];
459
+ for (const filePath of files) {
460
+ if (!fs.existsSync(filePath)) {
461
+ failedFiles.push(filePath);
462
+ continue;
463
+ }
297
464
  try {
298
465
  replacement.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
299
466
  } catch {
300
- // Skip unreadable files on restart.
467
+ failedFiles.push(filePath);
301
468
  }
302
469
  }
303
- return true;
470
+ for (const filePath of failedFiles) replacement.markFailedFile(filePath);
471
+ return { files, restarted: true };
304
472
  } catch {
305
473
  this.clients.delete(key);
306
474
  this.unavailable.set(key, "start-failed");
307
- return false;
475
+ // A bound-exceeded start may have spawned a live process; terminate
476
+ // the tree so the orphan cannot outlive the failed restart.
477
+ void replacement.forceKill().catch(() => {});
478
+ return { files, restarted: false };
308
479
  }
309
480
  }
310
481
 
@@ -347,41 +518,68 @@ export class LspManager {
347
518
  }
348
519
 
349
520
  /** Wait until the client that owns a file is query-ready, then run a light warm-up probe. */
350
- async waitUntilFileReady(filePath: string): Promise<LspClient | null> {
521
+ async waitUntilFileReady(
522
+ filePath: string,
523
+ control?: CodeRequestControl,
524
+ ): Promise<LspClient | null> {
525
+ throwIfCodeRequestInterrupted(control);
351
526
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
352
527
  const client = await this.getClientForFile(resolvedPath);
353
528
  if (!client) return null;
354
- await client.getReady();
355
- await this.warmSemanticProject(client, resolvedPath, true);
529
+ // The caller's wait stops on cancellation even though the shared
530
+ // readiness state keeps its own lifecycle.
531
+ await client.getReady(control);
532
+ await this.warmSemanticProject(client, resolvedPath, true, control);
533
+ throwIfCodeRequestInterrupted(control);
356
534
  return client;
357
535
  }
358
536
 
359
537
  /**
360
- * Wait until all started clients are query-ready, then warm one project file per client/root.
361
- * Returns the number of concrete clients that reached readiness.
538
+ * Wait until one started client is query-ready, then warm its project.
539
+ * Returns the number of concrete clients that are ready after the warm-up.
362
540
  */
363
- async waitUntilWorkspaceReady(): Promise<number> {
541
+ async waitUntilWorkspaceReady(control?: CodeRequestControl): Promise<number> {
542
+ throwIfCodeRequestInterrupted(control);
364
543
  const activeClients = Array.from(this.clients.values()).filter(
365
544
  (client) => client.status === "running",
366
545
  );
367
- await Promise.all(activeClients.map((client) => client.getReady()));
546
+ if (activeClients.length === 0) return 0;
368
547
 
369
- for (const client of activeClients) {
370
- const serverConfig = this.config.servers[client.name];
371
- if (!serverConfig) continue;
548
+ const alreadyReady = activeClients.find((client) => client.ready);
549
+ const firstReady =
550
+ alreadyReady ??
551
+ (await Promise.any(
552
+ activeClients.map(async (client) => {
553
+ await client.getReady(control);
554
+ if (client.status !== "running" || !client.ready) {
555
+ throw new Error("LSP client did not reach concrete readiness.");
556
+ }
557
+ return client;
558
+ }),
559
+ ).catch(() => null));
560
+ // Promise.any swallows individual rejections, so an interruption that
561
+ // raced the readiness wait must be re-raised before it degrades into a
562
+ // zero-ready outcome.
563
+ throwIfCodeRequestInterrupted(control);
564
+ if (!firstReady) return 0;
565
+
566
+ const serverConfig = this.config.servers[firstReady.name];
567
+ if (serverConfig) {
372
568
  const target = findWorkspaceSymbolWarmTargets(
373
- client.root,
569
+ firstReady.root,
374
570
  serverConfig.rootMarkers,
375
571
  serverConfig.fileTypes,
376
572
  )[0];
377
- if (!target) continue;
378
- await this.warmSemanticProject(client, target.file);
573
+ if (target) await this.warmSemanticProject(firstReady, target.file, false, control);
379
574
  }
380
- return activeClients.length;
575
+
576
+ throwIfCodeRequestInterrupted(control);
577
+ return activeClients.filter((client) => client.status === "running" && client.ready).length;
381
578
  }
382
579
  async syncFileAndGetDiagnostics(
383
580
  filePath: string,
384
581
  maxSeverity: number = 1,
582
+ control?: CodeRequestControl,
385
583
  ): Promise<CodeQueryResult<Diagnostic[]>> {
386
584
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
387
585
  const client = await this.getClientForFile(resolvedPath);
@@ -389,11 +587,11 @@ export class LspManager {
389
587
  return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
390
588
  }
391
589
  try {
392
- return {
393
- kind: "completed",
394
- data: await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity),
395
- };
590
+ return await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity, control);
396
591
  } catch (error) {
592
+ // Cancellation must propagate; a cancelled caller no longer awaits a
593
+ // diagnostic result or the file cleanup side effects.
594
+ if (isCodeRequestInterruption(error, control)) throw error;
397
595
  this.closeFile(resolvedPath);
398
596
  const detail = error instanceof Error ? error.message : String(error);
399
597
  return unavailableCodeQuery(`Diagnostic collection failed for ${resolvedPath}: ${detail}`);
@@ -407,9 +605,12 @@ export class LspManager {
407
605
  pruneMissingFiles(): string[] {
408
606
  return pruneMissingFilesFromClients(this.clients.values());
409
607
  }
410
- /** Re-sync all open documents across active clients and wait for diagnostics to settle. */
411
- async refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void> {
412
- await refreshOpenDiagnosticsForClients(this.clients.values(), options);
608
+ /** Re-sync all open documents across active clients and return exact evidence coverage. */
609
+ async refreshOpenDiagnostics(
610
+ options?: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl,
611
+ ): Promise<DiagnosticEvidenceSummary> {
612
+ const summary = await refreshOpenDiagnosticsForClients(this.clients.values(), options);
613
+ return this.toWorkspaceDiagnosticEvidence(summary.documents);
413
614
  }
414
615
 
415
616
  /** Clear cached pull-diagnostic result IDs across all clients. */
@@ -426,21 +627,59 @@ export class LspManager {
426
627
  }
427
628
  }
428
629
 
630
+ /**
631
+ * Record one workspace invalidation event and forward it to clients.
632
+ *
633
+ * Every call with a non-empty change batch advances the invalidation
634
+ * generation, so a repeated edit to the same file opens a fresh
635
+ * generation. Recovery passes forward changes through
636
+ * {@link notifyWorkspaceFileChanges} and never advance the generation.
637
+ */
638
+ noteWorkspaceChanges(changes: FileEvent[]): void {
639
+ if (changes.length > 0) this.invalidationEpoch++;
640
+ this.notifyWorkspaceFileChanges(changes);
641
+ }
642
+
643
+ /** Return per-route diagnostic evidence capability for recovery targeting. */
644
+ getClientDiagnosticRoutes(): Array<{
645
+ key: string;
646
+ supportsPull: boolean;
647
+ unconfirmedFiles: string[];
648
+ stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
649
+ }> {
650
+ const routes: Array<{
651
+ key: string;
652
+ supportsPull: boolean;
653
+ unconfirmedFiles: string[];
654
+ stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
655
+ }> = [];
656
+ for (const [key, client] of this.clients) {
657
+ if (client.status !== "running") continue;
658
+ const unconfirmedFiles = client
659
+ .getDiagnosticSnapshot()
660
+ .documents.filter((document) => document.status === "unconfirmed")
661
+ .map((document) => uriToFile(document.uri))
662
+ .filter((file) => this.isDiagnosticFile(file));
663
+ routes.push({
664
+ key,
665
+ supportsPull: client.hasDiagnosticProvider,
666
+ unconfirmedFiles,
667
+ stallSignal: client.getRecoveryStallSignal(),
668
+ });
669
+ }
670
+ return routes;
671
+ }
672
+
429
673
  /** Force a workspace-wide diagnostic recovery pass. */
430
674
  async recoverWorkspaceDiagnostics(options?: {
431
675
  changes?: FileEvent[];
432
676
  restartIfStillStale?: boolean;
433
677
  maxWaitMs?: number;
434
678
  quietMs?: number;
435
- }): Promise<{
436
- attemptedClients: number;
437
- restartedClients: number;
438
- staleAssessment: {
439
- suspected: boolean;
440
- matchedFiles: Array<{ file: string; diagnostics: Diagnostic[] }>;
441
- warning: string | null;
442
- };
443
- }> {
679
+ /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
680
+ initialEvidence?: DiagnosticEvidenceSummary;
681
+ control?: CodeRequestControl;
682
+ }): Promise<WorkspaceRecoveryResult> {
444
683
  return recoverWorkspaceDiagnosticsImpl(this, options);
445
684
  }
446
685
 
@@ -448,7 +687,12 @@ export class LspManager {
448
687
  async shutdownAll(): Promise<void> {
449
688
  const shutdowns = Array.from(this.clients.values()).map((c) => c.shutdown().catch(() => {}));
450
689
  await Promise.all(shutdowns);
690
+ // Dispose every client so pending or active progress tokens and
691
+ // readiness promises cannot outlive the manager.
692
+ for (const client of this.clients.values()) client.dispose();
451
693
  this.clients.clear();
694
+ this.clientGenerations.clear();
695
+ this.recoveryRestartEpochs.clear();
452
696
  this.unavailable.clear();
453
697
  this.knownRoots.clear();
454
698
  this.warmedWorkspaceSymbolProjects.clear();
@@ -456,6 +700,30 @@ export class LspManager {
456
700
  this.pendingWarmProbes.clear();
457
701
  }
458
702
  /** Get status of all servers. */
703
+ getRunningClientCount(): number {
704
+ return Array.from(this.clients.values()).filter((client) => client.status === "running").length;
705
+ }
706
+
707
+ /** Get the server names of all running clients, for recovery telemetry. */
708
+ getRunningClientNames(): string[] {
709
+ return Array.from(this.clients.values())
710
+ .filter((client) => client.status === "running")
711
+ .map((client) => client.name);
712
+ }
713
+
714
+ /** Check whether a path belongs to the configured diagnostic evidence scope. */
715
+ isDiagnosticFile(filePath: string): boolean {
716
+ const relative = path.relative(this.cwd, path.resolve(this.cwd, filePath));
717
+ return (
718
+ !shouldIgnoreLspPath(relative, this.cwd) &&
719
+ !isExcludedByPattern(relative, this.excludePatterns)
720
+ );
721
+ }
722
+
723
+ getDiagnosticEvidence(): DiagnosticEvidenceSummary {
724
+ return this.getDiagnosticSnapshot().evidence;
725
+ }
726
+
459
727
  getStatus(): ManagerStatus {
460
728
  this.pruneMissingFiles();
461
729
  const servers: ServerStatus[] = [];
@@ -538,41 +806,117 @@ export class LspManager {
538
806
  .filter((entry) => entry.openFiles.length > 0);
539
807
  return formatCoverageSummaryText(relevantEntries, maxServers, maxFiles);
540
808
  }
809
+ /**
810
+ * Capture one coherent diagnostic report from the current client snapshots.
811
+ *
812
+ * Summary and detailed projections share the same cache observation so a
813
+ * caller cannot combine entries from one generation with evidence from
814
+ * another.
815
+ */
816
+ getWorkspaceDiagnosticReport(maxSeverity: number = 2): WorkspaceDiagnosticReport {
817
+ this.pruneMissingFiles();
818
+ const snapshots = Array.from(this.clients.values()).map((client) =>
819
+ client.getDiagnosticSnapshot(),
820
+ );
821
+ const evidence = this.toWorkspaceDiagnosticEvidence(
822
+ snapshots.flatMap((snapshot) =>
823
+ snapshot.documents.map((document) => ({
824
+ file: relativeFilePathFromUri(document.uri, this.cwd),
825
+ status: document.status,
826
+ })),
827
+ ),
828
+ );
829
+ const current =
830
+ snapshots.every((snapshot) => snapshot.current) &&
831
+ evidence.documents.every((document) => document.status === "confirmed");
832
+ const fileDiags = new Map<string, { errors: number; warnings: number }>();
833
+ for (const snapshot of snapshots) {
834
+ for (const entry of snapshot.entries) {
835
+ collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
836
+ }
837
+ }
838
+
839
+ return {
840
+ summary: {
841
+ entries: Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts })),
842
+ current,
843
+ evidence,
844
+ },
845
+ outstanding: {
846
+ entries: collectOutstandingDiagnosticsDetailed(
847
+ snapshots.map((snapshot) => snapshot.entries),
848
+ this.cwd,
849
+ this.excludePatterns,
850
+ maxSeverity,
851
+ ),
852
+ current,
853
+ evidence,
854
+ },
855
+ };
856
+ }
857
+
541
858
  /** Get a diagnostic summary across all servers and files. */
542
859
  getDiagnosticSummary(): DiagnosticSummary[] {
860
+ return this.getDiagnosticSnapshot().entries;
861
+ }
862
+ getDiagnosticSnapshot(): {
863
+ entries: DiagnosticSummary[];
864
+ current: boolean;
865
+ evidence: DiagnosticEvidenceSummary;
866
+ } {
543
867
  this.pruneMissingFiles();
544
868
  const fileDiags = new Map<string, { errors: number; warnings: number }>();
869
+ const evidenceDocuments: DiagnosticEvidenceDocument[] = [];
870
+ let clientCurrent = true;
545
871
  for (const client of this.clients.values()) {
546
- for (const entry of client.getAllDiagnostics()) {
872
+ const snapshot = client.getDiagnosticSnapshot();
873
+ clientCurrent &&= snapshot.current;
874
+ for (const document of snapshot.documents) {
875
+ evidenceDocuments.push({
876
+ file: relativeFilePathFromUri(document.uri, this.cwd),
877
+ status: document.status,
878
+ });
879
+ }
880
+ for (const entry of snapshot.entries) {
547
881
  collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
548
882
  }
549
883
  }
550
- return Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts }));
884
+ const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
885
+ return {
886
+ entries: Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts })),
887
+ current:
888
+ clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
889
+ evidence,
890
+ };
551
891
  }
552
892
  /** Get outstanding diagnostics at or above the configured inline threshold. */
553
893
  getOutstandingDiagnosticSummary(maxSeverity: number = 1): OutstandingDiagnosticSummaryEntry[] {
894
+ return this.getOutstandingDiagnosticSummarySnapshot(maxSeverity).entries;
895
+ }
896
+ getOutstandingDiagnosticSummarySnapshot(maxSeverity: number = 1): {
897
+ entries: OutstandingDiagnosticSummaryEntry[];
898
+ current: boolean;
899
+ evidence: DiagnosticEvidenceSummary;
900
+ } {
554
901
  this.pruneMissingFiles();
555
- const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
556
- for (const client of this.clients.values()) {
557
- for (const entry of client.getAllDiagnostics()) {
558
- const file = relativeFilePathFromUri(entry.uri, this.cwd);
559
- if (shouldIgnoreLspPath(file, this.cwd)) continue;
560
- if (isExcludedByPattern(file, this.excludePatterns)) continue;
561
- const current = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
562
- const next = accumulateOutstandingDiagnostics(current, entry.diagnostics, maxSeverity);
563
- if (next.total > 0) {
564
- fileDiags.set(file, next);
565
- }
566
- }
567
- }
568
- return Array.from(fileDiags.values()).sort(
569
- (a, b) =>
570
- b.errors - a.errors ||
571
- b.warnings - a.warnings ||
572
- b.information - a.information ||
573
- b.hints - a.hints ||
574
- a.file.localeCompare(b.file),
902
+ const snapshots = Array.from(this.clients.values()).map((client) =>
903
+ client.getDiagnosticSnapshot(),
575
904
  );
905
+ const evidence = this.toWorkspaceDiagnosticEvidence(
906
+ collectClientDiagnosticEvidenceDocuments(snapshots, this.cwd),
907
+ );
908
+ const clientCurrent = snapshots.every((snapshot) => snapshot.current);
909
+ return {
910
+ entries: collectOutstandingDiagnosticSummaryEntries(
911
+ snapshots,
912
+ this.cwd,
913
+ this.excludePatterns,
914
+ maxSeverity,
915
+ ),
916
+ current:
917
+ clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
918
+ evidence,
919
+ };
576
920
  }
577
921
  getRelevantOutstandingDiagnosticsSummaryText(
578
922
  relevantPaths: string[],
@@ -591,28 +935,104 @@ export class LspManager {
591
935
  getOutstandingDiagnostics(
592
936
  maxSeverity: number = 1,
593
937
  ): Array<{ file: string; diagnostics: Diagnostic[] }> {
938
+ return this.getOutstandingDiagnosticsSnapshot(maxSeverity).entries;
939
+ }
940
+ getOutstandingDiagnosticsSnapshot(maxSeverity: number = 1): {
941
+ entries: Array<{ file: string; diagnostics: Diagnostic[] }>;
942
+ current: boolean;
943
+ evidence: DiagnosticEvidenceSummary;
944
+ } {
594
945
  this.pruneMissingFiles();
595
- return collectOutstandingDiagnosticsDetailed(
596
- this.clients.values(),
597
- this.cwd,
598
- this.excludePatterns,
599
- maxSeverity,
946
+ const snapshots = Array.from(this.clients.values()).map((client) =>
947
+ client.getDiagnosticSnapshot(),
948
+ );
949
+ const evidenceDocuments = snapshots.flatMap((snapshot) =>
950
+ snapshot.documents.map((document) => ({
951
+ file: relativeFilePathFromUri(document.uri, this.cwd),
952
+ status: document.status,
953
+ })),
954
+ );
955
+ const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
956
+ return {
957
+ entries: collectOutstandingDiagnosticsDetailed(
958
+ snapshots.map((snapshot) => snapshot.entries),
959
+ this.cwd,
960
+ this.excludePatterns,
961
+ maxSeverity,
962
+ ),
963
+ current:
964
+ snapshots.every((snapshot) => snapshot.current) &&
965
+ evidence.documents.every((document) => document.status === "confirmed"),
966
+ evidence,
967
+ };
968
+ }
969
+
970
+ /**
971
+ * Aggregate the tsconfig scope decision for every tracked file.
972
+ *
973
+ * The recovery telemetry surface uses this to report why tracked files are
974
+ * in or out of scope without re-deriving the scope logic. The returned
975
+ * entry list is bounded; counts are exact.
976
+ */
977
+ getScopeDecisionSummary(): ScopeDecisionSummary {
978
+ this.pruneMissingFiles();
979
+ const accumulator: ScopeDecisionAccumulator = {
980
+ entries: [],
981
+ counts: { included: 0, excluded: 0, noConfig: 0, outOfTree: 0 },
982
+ basisCounts: {},
983
+ };
984
+ let totalFiles = 0;
985
+
986
+ for (const client of this.clients.values()) {
987
+ for (const document of client.getDiagnosticSnapshot().documents) {
988
+ const file = relativeFilePathFromUri(document.uri, this.cwd);
989
+ totalFiles++;
990
+ accumulateScopeDecision(accumulator, file, getFileScopeDecision(file, this.cwd));
991
+ }
992
+ }
993
+
994
+ return {
995
+ caseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
996
+ counts: accumulator.counts,
997
+ basisCounts: accumulator.basisCounts,
998
+ entries: accumulator.entries,
999
+ totalFiles,
1000
+ };
1001
+ }
1002
+ private toWorkspaceDiagnosticEvidence(
1003
+ documents: readonly DiagnosticEvidenceDocument[],
1004
+ ): DiagnosticEvidenceSummary {
1005
+ const byFile = new Map<string, DiagnosticEvidenceDocument>();
1006
+ for (const document of documents) {
1007
+ const file = path.relative(this.cwd, path.resolve(this.cwd, document.file));
1008
+ if (shouldIgnoreLspPath(file, this.cwd)) continue;
1009
+ if (isExcludedByPattern(file, this.excludePatterns)) continue;
1010
+ const existing = byFile.get(file);
1011
+ if (!existing || evidenceStatusRank(document.status) > evidenceStatusRank(existing.status)) {
1012
+ byFile.set(file, { file, status: document.status });
1013
+ }
1014
+ }
1015
+ return summarizeDiagnosticEvidence(
1016
+ Array.from(byFile.values()).sort((a, b) => a.file.localeCompare(b.file)),
600
1017
  );
601
1018
  }
1019
+
602
1020
  async workspaceSymbol(
603
1021
  query: string,
1022
+ control?: CodeRequestControl,
604
1023
  ): Promise<CodeQueryResult<(SymbolInformation | WorkspaceSymbol)[]>> {
605
- const initial = await collectWorkspaceSymbols(this.clients.values(), query);
1024
+ const initial = await collectWorkspaceSymbols(this.clients.values(), query, control);
606
1025
  if (!initial.hasSupport || initial.results.length > 0) {
607
1026
  return workspaceSymbolCollectionResult(initial);
608
1027
  }
609
1028
 
610
- const warmed = await this.warmWorkspaceSymbolProjectsUntilResult(
611
- findWorkspaceSymbolWarmTargets,
612
- getWorkspaceSymbolWarmPosition,
613
- collectWorkspaceSymbols,
1029
+ const warmed = await this.warmWorkspaceSymbolProjectsUntilResult({
1030
+ findWarmTargets: findWorkspaceSymbolWarmTargets,
1031
+ getWarmPosition: getWorkspaceSymbolWarmPosition,
1032
+ collect: (clients, value) => collectWorkspaceSymbols(clients, value, control),
614
1033
  query,
615
- );
1034
+ control,
1035
+ });
616
1036
  return workspaceSymbolCollectionResult(warmed.collection ?? initial);
617
1037
  }
618
1038
  async ensureFileOpen(filePath: string): Promise<LspClient | null> {
@@ -629,18 +1049,20 @@ export class LspManager {
629
1049
  }
630
1050
 
631
1051
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: warm-up coordinates client/project iteration, targeted semantic nudges, and early-return queries in one place.
632
- private async warmWorkspaceSymbolProjectsUntilResult(
1052
+ private async warmWorkspaceSymbolProjectsUntilResult(options: {
633
1053
  findWarmTargets: (
634
1054
  root: string,
635
1055
  rootMarkers: string[],
636
1056
  fileTypes: string[],
637
- ) => Array<{ projectRoot: string; file: string }>,
1057
+ ) => Array<{ projectRoot: string; file: string }>;
638
1058
  getWarmPosition: (
639
1059
  symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
640
- ) => import("../config/types.ts").Position | null,
641
- collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>,
642
- query: string,
643
- ): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
1060
+ ) => import("../config/types.ts").Position | null;
1061
+ collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>;
1062
+ query: string;
1063
+ control?: CodeRequestControl;
1064
+ }): Promise<{ warmedAny: boolean; collection: WorkspaceSymbolCollection | null }> {
1065
+ const { findWarmTargets, getWarmPosition, collect, query, control } = options;
644
1066
  let warmedAny = false;
645
1067
 
646
1068
  for (const client of Array.from(this.clients.values())) {
@@ -667,13 +1089,20 @@ export class LspManager {
667
1089
  const openedClient = await this.ensureFileOpen(target.file);
668
1090
  if (!openedClient) continue;
669
1091
 
1092
+ // A cancelled pass must not mark projects warmed on its way out.
1093
+ throwIfCodeRequestInterrupted(control);
670
1094
  this.warmedWorkspaceSymbolProjects.add(projectKey);
671
1095
  warmedAny = true;
672
1096
 
673
- const symbols = await openedClient.documentSymbols(target.file);
1097
+ const symbols = control
1098
+ ? await openedClient.documentSymbols(target.file, control)
1099
+ : await openedClient.documentSymbols(target.file);
674
1100
  if (symbols.kind !== "unavailable") {
675
1101
  const hoverPosition = getWarmPosition(symbols.data);
676
- if (hoverPosition) await openedClient.hover(target.file, hoverPosition);
1102
+ if (hoverPosition) {
1103
+ if (control) await openedClient.hover(target.file, hoverPosition, control);
1104
+ else await openedClient.hover(target.file, hoverPosition);
1105
+ }
677
1106
  }
678
1107
 
679
1108
  const collected = await collect(this.clients.values(), query);
@@ -690,6 +1119,7 @@ export class LspManager {
690
1119
  client: LspClient,
691
1120
  filePath: string,
692
1121
  preferExactFile: boolean = false,
1122
+ control?: CodeRequestControl,
693
1123
  ): Promise<void> {
694
1124
  const serverConfig = this.config.servers[client.name];
695
1125
  if (!serverConfig) return;
@@ -706,14 +1136,16 @@ export class LspManager {
706
1136
 
707
1137
  const pending = this.pendingWarmProbes.get(projectKey);
708
1138
  if (pending) {
709
- await pending;
1139
+ // The shared probe keeps running for other consumers; only this
1140
+ // caller's wait stops when it cancels.
1141
+ await raceRequestControl(pending, control);
710
1142
  return;
711
1143
  }
712
1144
 
713
- const probe = this.performWarmProbe(client, resolvedFile, projectKey);
1145
+ const probe = this.performWarmProbe(client, resolvedFile, projectKey, control);
714
1146
  this.pendingWarmProbes.set(projectKey, probe);
715
1147
  try {
716
- await probe;
1148
+ await raceRequestControl(probe, control);
717
1149
  } finally {
718
1150
  this.pendingWarmProbes.delete(projectKey);
719
1151
  }
@@ -723,16 +1155,23 @@ export class LspManager {
723
1155
  _client: LspClient,
724
1156
  resolvedFile: string,
725
1157
  projectKey: string,
1158
+ control?: CodeRequestControl,
726
1159
  ): Promise<void> {
727
1160
  const openedClient = await this.ensureFileOpen(resolvedFile);
728
1161
  if (!openedClient) return;
729
1162
 
730
1163
  this.warmedSemanticProjects.add(projectKey);
731
1164
 
732
- const symbols = await openedClient.documentSymbols(resolvedFile);
1165
+ // The probe is shared state for concurrent callers. One caller's
1166
+ // cancellation must not cancel the shared in-flight queries, so only the
1167
+ // Debug Operation ID is forwarded; each caller races its own wait above.
1168
+ const sharedControl = { operationId: control?.operationId };
1169
+ const symbols = await openedClient.documentSymbols(resolvedFile, sharedControl);
733
1170
  if (symbols.kind === "unavailable") return;
734
1171
  const hoverPosition = getWorkspaceSymbolWarmPosition(symbols.data);
735
- if (hoverPosition) await openedClient.hover(resolvedFile, hoverPosition);
1172
+ if (hoverPosition) {
1173
+ await openedClient.hover(resolvedFile, hoverPosition, sharedControl);
1174
+ }
736
1175
  }
737
1176
 
738
1177
  private hasOpenFileInProject(client: LspClient, projectRoot: string): boolean {
@@ -770,3 +1209,54 @@ export class LspManager {
770
1209
  }
771
1210
  }
772
1211
  }
1212
+
1213
+ function collectClientDiagnosticEvidenceDocuments(
1214
+ snapshots: Iterable<ClientDiagnosticSnapshot>,
1215
+ cwd: string,
1216
+ ): DiagnosticEvidenceDocument[] {
1217
+ return Array.from(snapshots).flatMap((snapshot) =>
1218
+ snapshot.documents.map((document) => ({
1219
+ file: relativeFilePathFromUri(document.uri, cwd),
1220
+ status: document.status,
1221
+ })),
1222
+ );
1223
+ }
1224
+
1225
+ function collectOutstandingDiagnosticSummaryEntries(
1226
+ snapshots: Iterable<ClientDiagnosticSnapshot>,
1227
+ cwd: string,
1228
+ excludePatterns: string[],
1229
+ maxSeverity: number,
1230
+ ): OutstandingDiagnosticSummaryEntry[] {
1231
+ const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
1232
+ for (const snapshot of snapshots) {
1233
+ for (const entry of snapshot.entries) {
1234
+ const file = relativeFilePathFromUri(entry.uri, cwd);
1235
+ if (shouldIgnoreLspPath(file, cwd) || isExcludedByPattern(file, excludePatterns)) continue;
1236
+ const existing = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
1237
+ const next = accumulateOutstandingDiagnostics(existing, entry.diagnostics, maxSeverity);
1238
+ if (next.total > 0) fileDiags.set(file, next);
1239
+ }
1240
+ }
1241
+ return Array.from(fileDiags.values()).sort(
1242
+ (a, b) =>
1243
+ b.errors - a.errors ||
1244
+ b.warnings - a.warnings ||
1245
+ b.information - a.information ||
1246
+ b.hints - a.hints ||
1247
+ a.file.localeCompare(b.file),
1248
+ );
1249
+ }
1250
+
1251
+ function evidenceStatusRank(status: DiagnosticEvidenceDocument["status"]): number {
1252
+ switch (status) {
1253
+ case "confirmed":
1254
+ return 1;
1255
+ case "unconfirmed":
1256
+ return 2;
1257
+ case "failed":
1258
+ return 3;
1259
+ case "removed":
1260
+ return 4;
1261
+ }
1262
+ }