@mrclrchtr/supi-code-intelligence 4.9.0 → 5.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 (215) hide show
  1. package/README.md +2 -1
  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 +1 -1
  10. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  13. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  14. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  15. package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  30. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +215 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +370 -266
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
  46. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +238 -11
  47. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
  48. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  49. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  52. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
  54. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  55. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
  56. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  58. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  59. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  62. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  63. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
  64. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
  66. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  67. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +26 -6
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -3
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  127. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  128. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  129. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  130. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  131. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  132. package/package.json +5 -5
  133. package/src/analysis/health/diagnostics.ts +236 -24
  134. package/src/analysis/health/recovery.ts +6 -2
  135. package/src/analysis/provider.ts +58 -0
  136. package/src/analysis/readiness.ts +30 -9
  137. package/src/analysis/refactor/apply.ts +59 -20
  138. package/src/analysis/refactor/mutation-authority.ts +143 -0
  139. package/src/analysis/refactor/position.ts +47 -0
  140. package/src/analysis/refactor/safety.ts +18 -10
  141. package/src/analysis/relations/callees.ts +9 -1
  142. package/src/analysis/relations/types.ts +5 -2
  143. package/src/analysis/search/ast-scan-timing.ts +9 -4
  144. package/src/analysis/search/ast-scan.ts +1 -1
  145. package/src/analysis/search/deadline.ts +2 -2
  146. package/src/analysis/search/pattern-analysis.ts +46 -32
  147. package/src/analysis/search/pattern.ts +12 -2
  148. package/src/analysis/target/anchored.ts +20 -11
  149. package/src/analysis/target/file.ts +18 -11
  150. package/src/analysis/target/identity.ts +19 -3
  151. package/src/analysis/target/symbol.ts +36 -15
  152. package/src/api.ts +4 -0
  153. package/src/diagnostics/evidence.ts +51 -0
  154. package/src/session/capability-adapter.ts +12 -3
  155. package/src/session/find-workflow.ts +17 -7
  156. package/src/session/graph/collect.ts +18 -2
  157. package/src/session/graph-workflow.ts +19 -12
  158. package/src/session/health-refresh.ts +148 -0
  159. package/src/session/health-types.ts +37 -15
  160. package/src/session/health-workflow.ts +37 -39
  161. package/src/session/inspect/collect.ts +20 -3
  162. package/src/session/inspect-workflow.ts +10 -5
  163. package/src/session/orientation/collect.ts +10 -1
  164. package/src/session/orientation/context-facts.ts +3 -1
  165. package/src/session/orientation/context-sections.ts +20 -7
  166. package/src/session/orientation/gather.ts +17 -11
  167. package/src/session/orientation-types.ts +2 -1
  168. package/src/session/orientation-workflow.ts +15 -8
  169. package/src/session/refactor-plans.ts +2 -0
  170. package/src/session/refactor-workflow.ts +59 -14
  171. package/src/session/session.ts +4 -1
  172. package/src/session/target-workflow.ts +54 -23
  173. package/src/session/workflow-control.ts +7 -3
  174. package/src/substrate/lsp/lifecycle.ts +26 -46
  175. package/src/substrate/lsp/maintenance.ts +199 -25
  176. package/src/substrate/lsp/state.ts +2 -2
  177. package/src/substrate/workspace-provider-host.ts +25 -3
  178. package/src/tool/find/execute.ts +7 -1
  179. package/src/tool/find/render.ts +20 -3
  180. package/src/tool/find/tui.ts +23 -21
  181. package/src/tool/graph/execute.ts +23 -3
  182. package/src/tool/graph/tui.ts +59 -46
  183. package/src/tool/health/execute.ts +7 -2
  184. package/src/tool/health/markdown.ts +56 -14
  185. package/src/tool/health/refresh-status.ts +105 -0
  186. package/src/tool/health/tui.ts +95 -98
  187. package/src/tool/infra/workflow-control.ts +8 -0
  188. package/src/tool/inspect/execute.ts +7 -1
  189. package/src/tool/inspect/tui.ts +26 -18
  190. package/src/tool/orientation/execute.ts +21 -3
  191. package/src/tool/orientation/tui.ts +51 -35
  192. package/src/tool/refactor-apply/execute.ts +3 -0
  193. package/src/tool/refactor-apply/tui.ts +43 -25
  194. package/src/tool/refactor-plan/execute.ts +4 -0
  195. package/src/tool/refactor-plan/tui.ts +33 -20
  196. package/src/tool/register.ts +127 -22
  197. package/src/tool/resolve/execute.ts +18 -1
  198. package/src/tool/resolve/tui.ts +42 -24
  199. package/src/tool/result/display.ts +107 -0
  200. package/src/tool/result/errors.ts +20 -17
  201. package/src/tool/result/find.ts +52 -8
  202. package/src/tool/result/graph.ts +44 -1
  203. package/src/tool/result/health.ts +53 -1
  204. package/src/tool/result/inspect.ts +113 -1
  205. package/src/tool/result/orientation.ts +66 -14
  206. package/src/tool/result/refactor.ts +36 -4
  207. package/src/tool/result/resolve.ts +44 -7
  208. package/src/tool/result/types.ts +20 -0
  209. package/src/types/execution.ts +18 -5
  210. package/src/types/index.ts +8 -1
  211. package/src/ui/status-command.ts +5 -3
  212. package/src/ui/status-overlay.ts +4 -0
  213. package/src/ui/tui/common.ts +108 -10
  214. package/src/ui/tui/display.ts +48 -0
  215. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -247
@@ -1,4 +1,8 @@
1
- import { getDefaultWorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
1
+ import {
2
+ type CodeRequestControl,
3
+ getDefaultWorkspaceRuntime,
4
+ throwIfCodeRequestInterrupted,
5
+ } from "@mrclrchtr/supi-code-runtime/api";
2
6
  import {
3
7
  getWorkspaceLspRuntime,
4
8
  type SemanticReadinessResult,
@@ -14,7 +18,10 @@ export async function ensureSemanticReadiness(
14
18
  cwd: string,
15
19
  scope: SemanticStartupScope,
16
20
  timeoutMs: number = DEFAULT_SEMANTIC_STARTUP_TIMEOUT_MS,
21
+ control?: CodeRequestControl,
17
22
  ): Promise<SemanticReadinessResult> {
23
+ // A cancelled caller stops before any readiness work starts.
24
+ throwIfCodeRequestInterrupted(control);
18
25
  const workspace = getDefaultWorkspaceRuntime().getWorkspace(cwd);
19
26
  if (workspace.semantic.provider === null) {
20
27
  return {
@@ -24,7 +31,7 @@ export async function ensureSemanticReadiness(
24
31
  }
25
32
 
26
33
  if (workspace.semantic.state.kind === "ready") {
27
- return resolveReadySemanticState(cwd, scope, timeoutMs);
34
+ return resolveReadySemanticState(cwd, scope, timeoutMs, control);
28
35
  }
29
36
 
30
37
  if (workspace.semantic.state.kind !== "pending") {
@@ -34,13 +41,15 @@ export async function ensureSemanticReadiness(
34
41
  };
35
42
  }
36
43
 
37
- const deadline = Date.now() + timeoutMs;
44
+ // The request deadline bounds the internal readiness budget when it is
45
+ // earlier than the relative startup timeout.
46
+ const deadline = Math.min(Date.now() + timeoutMs, control?.deadline ?? Infinity);
38
47
 
39
48
  const remainingAfterService = deadline - Date.now();
40
49
  if (remainingAfterService <= 0) {
41
50
  return { kind: "timeout" };
42
51
  }
43
- const lspState = await resolveSemanticServiceState(cwd, remainingAfterService);
52
+ const lspState = await resolveSemanticServiceState(cwd, remainingAfterService, control);
44
53
  if (lspState.kind === "pending") {
45
54
  return { kind: "timeout" };
46
55
  }
@@ -58,8 +67,13 @@ export async function ensureSemanticReadiness(
58
67
  if (remainingAfterLsp <= 0) {
59
68
  return { kind: "timeout" };
60
69
  }
61
- return scope.kind === "workspace"
62
- ? lspState.runtime.waitUntilReadyForWorkspace({ timeoutMs: remainingAfterLsp })
70
+ if (scope.kind === "workspace") {
71
+ return control
72
+ ? lspState.runtime.waitUntilReadyForWorkspace({ timeoutMs: remainingAfterLsp }, control)
73
+ : lspState.runtime.waitUntilReadyForWorkspace({ timeoutMs: remainingAfterLsp });
74
+ }
75
+ return control
76
+ ? lspState.runtime.waitUntilReadyForFile(scope.file, { timeoutMs: remainingAfterLsp }, control)
63
77
  : lspState.runtime.waitUntilReadyForFile(scope.file, { timeoutMs: remainingAfterLsp });
64
78
  }
65
79
 
@@ -67,11 +81,17 @@ function resolveReadySemanticState(
67
81
  cwd: string,
68
82
  scope: SemanticStartupScope,
69
83
  timeoutMs: number,
84
+ control?: CodeRequestControl,
70
85
  ): Promise<SemanticReadinessResult> | SemanticReadinessResult {
71
86
  const lspState = getWorkspaceLspRuntime(cwd);
72
87
  if (lspState.kind === "ready") {
73
- return scope.kind === "workspace"
74
- ? lspState.runtime.waitUntilReadyForWorkspace({ timeoutMs })
88
+ if (scope.kind === "workspace") {
89
+ return control
90
+ ? lspState.runtime.waitUntilReadyForWorkspace({ timeoutMs }, control)
91
+ : lspState.runtime.waitUntilReadyForWorkspace({ timeoutMs });
92
+ }
93
+ return control
94
+ ? lspState.runtime.waitUntilReadyForFile(scope.file, { timeoutMs }, control)
75
95
  : lspState.runtime.waitUntilReadyForFile(scope.file, { timeoutMs });
76
96
  }
77
97
  return {
@@ -86,8 +106,9 @@ function resolveReadySemanticState(
86
106
  async function resolveSemanticServiceState(
87
107
  cwd: string,
88
108
  timeoutMs: number,
109
+ control?: CodeRequestControl,
89
110
  ): Promise<WorkspaceLspRuntimeState> {
90
111
  const initialState = getWorkspaceLspRuntime(cwd);
91
112
  if (initialState.kind !== "pending") return initialState;
92
- return waitForWorkspaceLspRuntime(cwd, timeoutMs);
113
+ return waitForWorkspaceLspRuntime(cwd, timeoutMs, control);
93
114
  }
@@ -16,7 +16,8 @@ import { readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:
16
16
  import { basename, dirname, join } from "node:path";
17
17
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
18
18
  import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
19
- import { compareCodePositions } from "./position.ts";
19
+ import { revalidateMutationAuthority } from "./mutation-authority.ts";
20
+ import { compareCodePositions, createLogicalLineIndex } from "./position.ts";
20
21
  import { validateEditAgainstFiles } from "./safety.ts";
21
22
 
22
23
  export type ApplyResult =
@@ -24,8 +25,12 @@ export type ApplyResult =
24
25
  | { kind: "error"; reason: string };
25
26
 
26
27
  export interface ApplyOptions {
28
+ /** Canonical roots authorized by the semantic provider that made the plan. */
29
+ authorizedMutationRoots: readonly string[];
27
30
  /** Expected SHA-256 fingerprints per file, validated after queue acquisition. */
28
31
  expectedFingerprints?: ReadonlyMap<string, string>;
32
+ /** Current LSP document version, or null when the document is not open. */
33
+ getOpenDocumentVersion?: (file: string) => number | null;
29
34
  }
30
35
 
31
36
  /**
@@ -49,16 +54,24 @@ export interface ApplyOptions {
49
54
  */
50
55
  export async function applyWorkspaceEdit(
51
56
  edit: WorkspaceEdit,
52
- options: ApplyOptions = {},
57
+ options: ApplyOptions,
53
58
  ): Promise<ApplyResult> {
54
59
  const grouped = groupEditsByFile(edit.edits);
55
60
  const files = [...grouped.keys()].sort();
56
61
  return withAllMutationQueues(files, async () => {
62
+ const authority = revalidateMutationAuthority(files, options.authorizedMutationRoots);
63
+ if (authority.kind === "unavailable") {
64
+ return { kind: "error", reason: authority.reason };
65
+ }
66
+
57
67
  if (options.expectedFingerprints) {
58
68
  const freshness = validateFingerprints(files, options.expectedFingerprints);
59
69
  if (!freshness.ok) return { kind: "error", reason: freshness.reason };
60
70
  }
61
71
 
72
+ const documentState = validateDocumentPreconditions(edit, options.getOpenDocumentVersion);
73
+ if (!documentState.ok) return { kind: "error", reason: documentState.reason };
74
+
62
75
  const validation = validateEditAgainstFiles(edit);
63
76
  if (!validation.safe) {
64
77
  return { kind: "error", reason: validation.reason };
@@ -76,6 +89,37 @@ export async function applyWorkspaceEdit(
76
89
  });
77
90
  }
78
91
 
92
+ function validateDocumentPreconditions(
93
+ edit: WorkspaceEdit,
94
+ getOpenDocumentVersion: ApplyOptions["getOpenDocumentVersion"],
95
+ ): { ok: true } | { ok: false; reason: string } {
96
+ const preconditions = edit.documentPreconditions ?? [];
97
+ if (preconditions.length === 0) return { ok: true };
98
+ if (!getOpenDocumentVersion) {
99
+ return {
100
+ ok: false,
101
+ reason: "Stored document preconditions cannot be revalidated.",
102
+ };
103
+ }
104
+
105
+ for (const precondition of preconditions) {
106
+ const currentVersion = getOpenDocumentVersion(precondition.file);
107
+ if (precondition.kind === "open-document-version" && currentVersion !== precondition.version) {
108
+ return {
109
+ ok: false,
110
+ reason: `File ${precondition.file} open document version has changed since the plan was generated.`,
111
+ };
112
+ }
113
+ if (precondition.kind === "disk-content" && currentVersion !== null) {
114
+ return {
115
+ ok: false,
116
+ reason: `File ${precondition.file} is now open and no longer uses disk content as its master.`,
117
+ };
118
+ }
119
+ }
120
+ return { ok: true };
121
+ }
122
+
79
123
  function validateFingerprints(
80
124
  files: string[],
81
125
  expected: ReadonlyMap<string, string>,
@@ -158,18 +202,21 @@ function buildTransformedContents(
158
202
  }
159
203
 
160
204
  function applyEditsToContent(content: string, edits: FileEdit[]): string {
161
- const lines = content.split("\n");
162
- const sortedEdits = [...edits].sort(
163
- (left, right) =>
164
- // Descending by start position so later edits don't shift earlier ones.
165
- compareCodePositions(right.range.start, left.range.start) ||
166
- compareCodePositions(right.range.end, left.range.end),
167
- );
205
+ const lineIndex = createLogicalLineIndex(content);
206
+ const sortedEdits = edits
207
+ .map((edit, protocolIndex) => ({ edit, protocolIndex }))
208
+ .sort(
209
+ (left, right) =>
210
+ // Descending positions prevent shifts. Reverse protocol order at one
211
+ // position so sequential inserts appear in their supplied order.
212
+ compareCodePositions(right.edit.range.start, left.edit.range.start) ||
213
+ right.protocolIndex - left.protocolIndex,
214
+ );
168
215
 
169
216
  let updated = content;
170
- for (const fileEdit of sortedEdits) {
171
- const startOffset = toOffset(lines, fileEdit.range.start.line, fileEdit.range.start.character);
172
- const endOffset = toOffset(lines, fileEdit.range.end.line, fileEdit.range.end.character);
217
+ for (const { edit: fileEdit } of sortedEdits) {
218
+ const startOffset = lineIndex.offsetAt(fileEdit.range.start);
219
+ const endOffset = lineIndex.offsetAt(fileEdit.range.end);
173
220
  updated = updated.slice(0, startOffset) + fileEdit.newText + updated.slice(endOffset);
174
221
  }
175
222
 
@@ -266,14 +313,6 @@ function rollbackCommittedFiles(
266
313
  }
267
314
  }
268
315
 
269
- function toOffset(lines: string[], line: number, character: number): number {
270
- let offset = 0;
271
- for (let index = 0; index < line && index < lines.length; index++) {
272
- offset += lines[index].length + 1;
273
- }
274
- return offset + character;
275
- }
276
-
277
316
  function toErrorMessage(error: unknown): string {
278
317
  return error instanceof Error ? error.message : String(error);
279
318
  }
@@ -0,0 +1,143 @@
1
+ import { lstatSync, realpathSync, statSync } from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export type MutationAuthorityResult =
5
+ | { kind: "authorized"; canonicalRoots: string[] }
6
+ | { kind: "unavailable"; reason: string };
7
+
8
+ /**
9
+ * Establish canonical provider roots and verify that all mutation files are
10
+ * regular files inside at least one root.
11
+ */
12
+ export function establishMutationAuthority(
13
+ files: readonly string[],
14
+ providerRoots: readonly string[],
15
+ ): MutationAuthorityResult {
16
+ const roots = canonicalizeProviderRoots(providerRoots);
17
+ if (roots.kind === "unavailable") return roots;
18
+
19
+ const fileValidation = validateFiles(files, roots.canonicalRoots);
20
+ return fileValidation.kind === "unavailable" ? fileValidation : roots;
21
+ }
22
+
23
+ /** Revalidate stored canonical roots and mutation files immediately before mutation. */
24
+ export function revalidateMutationAuthority(
25
+ files: readonly string[],
26
+ canonicalRoots: readonly string[],
27
+ ): MutationAuthorityResult {
28
+ if (canonicalRoots.length === 0) {
29
+ return { kind: "unavailable", reason: "Mutation plan has no authorized provider root." };
30
+ }
31
+
32
+ for (const root of canonicalRoots) {
33
+ if (!path.isAbsolute(root) || path.normalize(root) !== root) {
34
+ return { kind: "unavailable", reason: `Stored mutation root "${root}" is not canonical.` };
35
+ }
36
+ try {
37
+ if (!lstatSync(root).isDirectory() || realpathSync(root) !== root) {
38
+ return { kind: "unavailable", reason: `Stored mutation root "${root}" has changed.` };
39
+ }
40
+ } catch {
41
+ return { kind: "unavailable", reason: `Stored mutation root "${root}" is unavailable.` };
42
+ }
43
+ }
44
+
45
+ return validateFiles(files, canonicalRoots);
46
+ }
47
+
48
+ function canonicalizeProviderRoots(providerRoots: readonly string[]): MutationAuthorityResult {
49
+ if (providerRoots.length === 0) {
50
+ return { kind: "unavailable", reason: "Semantic route did not authorize a mutation root." };
51
+ }
52
+
53
+ const canonicalRoots = new Set<string>();
54
+ for (const root of providerRoots) {
55
+ if (!path.isAbsolute(root)) {
56
+ return { kind: "unavailable", reason: `Mutation root "${root}" is not absolute.` };
57
+ }
58
+ try {
59
+ const canonicalRoot = realpathSync(root);
60
+ if (!statSync(canonicalRoot).isDirectory()) {
61
+ return {
62
+ kind: "unavailable",
63
+ reason: `Mutation root "${root}" is not a directory.`,
64
+ };
65
+ }
66
+ canonicalRoots.add(canonicalRoot);
67
+ } catch {
68
+ return {
69
+ kind: "unavailable",
70
+ reason: `Mutation root "${root}" cannot be resolved to a canonical directory.`,
71
+ };
72
+ }
73
+ }
74
+
75
+ return { kind: "authorized", canonicalRoots: [...canonicalRoots].sort() };
76
+ }
77
+
78
+ function validateFiles(
79
+ files: readonly string[],
80
+ canonicalRoots: readonly string[],
81
+ ): MutationAuthorityResult {
82
+ for (const file of new Set(files)) {
83
+ const validation = validateFile(file, canonicalRoots);
84
+ if (validation) return { kind: "unavailable", reason: validation };
85
+ }
86
+ return { kind: "authorized", canonicalRoots: [...canonicalRoots] };
87
+ }
88
+
89
+ function validateFile(file: string, canonicalRoots: readonly string[]): string | null {
90
+ if (!path.isAbsolute(file) || path.normalize(file) !== file) {
91
+ return `Mutation file "${file}" is not a normalized absolute path.`;
92
+ }
93
+
94
+ let canonicalParent: string;
95
+ try {
96
+ canonicalParent = realpathSync(path.dirname(file));
97
+ } catch {
98
+ return `Mutation file "${file}" does not have a resolvable canonical parent.`;
99
+ }
100
+
101
+ try {
102
+ if (!lstatSync(file).isFile()) {
103
+ return `Mutation file "${file}" is not a regular file.`;
104
+ }
105
+ } catch {
106
+ return `Mutation file "${file}" is not a readable regular file.`;
107
+ }
108
+
109
+ let canonicalFile: string;
110
+ try {
111
+ canonicalFile = realpathSync(file);
112
+ } catch {
113
+ return `Mutation file "${file}" cannot be resolved to a canonical file.`;
114
+ }
115
+
116
+ const expectedCanonicalFile = path.join(canonicalParent, path.basename(file));
117
+ if (path.resolve(canonicalFile) !== path.resolve(expectedCanonicalFile)) {
118
+ return `Mutation file "${file}" resolves through an unsupported file link.`;
119
+ }
120
+ if (!canonicalRoots.some((root) => isWithinOrEqual(root, canonicalFile))) {
121
+ return `Mutation file "${file}" is outside the authorized provider roots.`;
122
+ }
123
+ return null;
124
+ }
125
+
126
+ function isWithinOrEqual(root: string, candidate: string): boolean {
127
+ return isMutationPathWithinRoot(root, candidate, path);
128
+ }
129
+
130
+ /** Check canonical path containment with the selected platform path rules. */
131
+ export function isMutationPathWithinRoot(
132
+ root: string,
133
+ candidate: string,
134
+ pathApi: Pick<typeof path, "isAbsolute" | "relative" | "sep">,
135
+ ): boolean {
136
+ const difference = pathApi.relative(root, candidate);
137
+ return (
138
+ difference === "" ||
139
+ (difference !== ".." &&
140
+ !difference.startsWith(`..${pathApi.sep}`) &&
141
+ !pathApi.isAbsolute(difference))
142
+ );
143
+ }
@@ -1,5 +1,52 @@
1
1
  import type { CodePosition } from "@mrclrchtr/supi-code-runtime/api";
2
2
 
3
+ interface IndexedLine {
4
+ readonly startOffset: number;
5
+ readonly length: number;
6
+ }
7
+
8
+ /** Logical source lines with offsets into the unchanged source string. */
9
+ export interface LogicalLineIndex {
10
+ readonly lineCount: number;
11
+ /** Get one logical line length in UTF-16 code units. */
12
+ lineLength(line: number): number | undefined;
13
+ /** Convert a validated zero-based UTF-16 position to a source-string offset. */
14
+ offsetAt(position: CodePosition): number;
15
+ }
16
+
17
+ /**
18
+ * Index LF, CRLF, and bare-CR lines without normalizing source bytes.
19
+ *
20
+ * JavaScript string offsets and lengths use UTF-16 code units, which are the
21
+ * only LSP coordinate encoding that the refactor path supports.
22
+ */
23
+ export function createLogicalLineIndex(source: string): LogicalLineIndex {
24
+ const lines: IndexedLine[] = [];
25
+ let lineStart = 0;
26
+
27
+ for (let offset = 0; offset < source.length; offset++) {
28
+ const codeUnit = source.charCodeAt(offset);
29
+ if (codeUnit !== 0x0a && codeUnit !== 0x0d) continue;
30
+
31
+ lines.push({ startOffset: lineStart, length: offset - lineStart });
32
+ if (codeUnit === 0x0d && source.charCodeAt(offset + 1) === 0x0a) offset++;
33
+ lineStart = offset + 1;
34
+ }
35
+ lines.push({ startOffset: lineStart, length: source.length - lineStart });
36
+
37
+ return {
38
+ lineCount: lines.length,
39
+ lineLength: (line) => lines[line]?.length,
40
+ offsetAt: (position) => {
41
+ const line = lines[position.line];
42
+ if (!line || position.character < 0 || position.character > line.length) {
43
+ throw new RangeError("Position is outside the logical source lines");
44
+ }
45
+ return line.startOffset + position.character;
46
+ },
47
+ };
48
+ }
49
+
3
50
  /** Compare zero-based source positions lexicographically by line, then character. */
4
51
  export function compareCodePositions(left: CodePosition, right: CodePosition): number {
5
52
  return left.line - right.line || left.character - right.character;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { readFileSync } from "node:fs";
9
9
  import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
10
- import { compareCodePositions } from "./position.ts";
10
+ import { compareCodePositions, createLogicalLineIndex, type LogicalLineIndex } from "./position.ts";
11
11
 
12
12
  export type ValidationResult = { safe: true } | { safe: false; reason: string };
13
13
 
@@ -76,12 +76,12 @@ export function validateEditAgainstFiles(edit: WorkspaceEdit): ValidationResult
76
76
  };
77
77
  }
78
78
 
79
- const lines = content.split("\n");
79
+ const lineIndex = createLogicalLineIndex(content);
80
80
  for (const fileEdit of fileEdits) {
81
- const lineValidation = validateLineBounds(file, fileEdit, lines);
81
+ const lineValidation = validateLineBounds(file, fileEdit, lineIndex);
82
82
  if (!lineValidation.safe) return lineValidation;
83
83
 
84
- const characterValidation = validateCharacterBounds(file, fileEdit, lines);
84
+ const characterValidation = validateCharacterBounds(file, fileEdit, lineIndex);
85
85
  if (!characterValidation.safe) return characterValidation;
86
86
  }
87
87
  }
@@ -89,19 +89,27 @@ export function validateEditAgainstFiles(edit: WorkspaceEdit): ValidationResult
89
89
  return { safe: true };
90
90
  }
91
91
 
92
- function validateLineBounds(file: string, edit: FileEdit, lines: string[]): ValidationResult {
93
- const maxLine = lines.length - 1;
92
+ function validateLineBounds(
93
+ file: string,
94
+ edit: FileEdit,
95
+ lineIndex: LogicalLineIndex,
96
+ ): ValidationResult {
97
+ const maxLine = lineIndex.lineCount - 1;
94
98
  if (edit.range.start.line > maxLine || edit.range.end.line > maxLine) {
95
99
  return {
96
100
  safe: false,
97
- reason: `Edit in file "${file}" references line ${Math.max(edit.range.start.line, edit.range.end.line)}, but the file has only ${lines.length} line${lines.length === 1 ? "" : "s"}`,
101
+ reason: `Edit in file "${file}" references line ${Math.max(edit.range.start.line, edit.range.end.line)}, but the file has only ${lineIndex.lineCount} line${lineIndex.lineCount === 1 ? "" : "s"}`,
98
102
  };
99
103
  }
100
104
  return { safe: true };
101
105
  }
102
106
 
103
- function validateCharacterBounds(file: string, edit: FileEdit, lines: string[]): ValidationResult {
104
- const startLineLength = lines[edit.range.start.line]?.length ?? 0;
107
+ function validateCharacterBounds(
108
+ file: string,
109
+ edit: FileEdit,
110
+ lineIndex: LogicalLineIndex,
111
+ ): ValidationResult {
112
+ const startLineLength = lineIndex.lineLength(edit.range.start.line) ?? 0;
105
113
  if (edit.range.start.character > startLineLength) {
106
114
  return {
107
115
  safe: false,
@@ -109,7 +117,7 @@ function validateCharacterBounds(file: string, edit: FileEdit, lines: string[]):
109
117
  };
110
118
  }
111
119
 
112
- const endLineLength = lines[edit.range.end.line]?.length ?? 0;
120
+ const endLineLength = lineIndex.lineLength(edit.range.end.line) ?? 0;
113
121
  if (edit.range.end.character > endLineLength) {
114
122
  return {
115
123
  safe: false,
@@ -2,6 +2,7 @@
2
2
  * Structural callee lookup — finds direct calls in the enclosing scope at a target position.
3
3
  */
4
4
 
5
+ import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
5
6
  import type { CalleeEntry, CalleeScope, RelationsServiceDeps } from "./types.ts";
6
7
 
7
8
  export interface CalleesResult {
@@ -26,12 +27,19 @@ export async function collectCallees(
26
27
  deps: RelationsServiceDeps,
27
28
  maxResults?: number,
28
29
  depth: "direct" | "deep" = "direct",
30
+ control?: CodeRequestControl,
29
31
  ): Promise<CalleesResult> {
30
32
  if (!deps.provider?.calleesAt) {
31
33
  return unavailableCallees(targetName);
32
34
  }
33
35
 
34
- const result = await deps.provider.calleesAt(targetFile, targetLine, targetCharacter, depth);
36
+ const depthOrOptions = control ? { depth, control } : depth;
37
+ const result = await deps.provider.calleesAt(
38
+ targetFile,
39
+ targetLine,
40
+ targetCharacter,
41
+ depthOrOptions,
42
+ );
35
43
  if (result.kind !== "success" || !result.data) {
36
44
  return unavailableCallees(targetName);
37
45
  }
@@ -1,4 +1,4 @@
1
- import type { SemanticProvider } from "@mrclrchtr/supi-code-runtime/api";
1
+ import type { CodeRequestControl, SemanticProvider } from "@mrclrchtr/supi-code-runtime/api";
2
2
 
3
3
  /**
4
4
  * Shared types for the relations analysis modules.
@@ -58,7 +58,10 @@ export interface RelationsServiceDeps {
58
58
  file: string,
59
59
  line: number,
60
60
  character: number,
61
- depth?: "direct" | "deep",
61
+ depthOrOptions?:
62
+ | "direct"
63
+ | "deep"
64
+ | { depth?: "direct" | "deep"; control?: CodeRequestControl },
62
65
  ) => Promise<{
63
66
  kind: string;
64
67
  data?: {
@@ -1,9 +1,10 @@
1
+ import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
1
2
  import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
3
+ import { truncateIdentity } from "@mrclrchtr/supi-lsp/debug-telemetry";
2
4
  import type { StructuralSearchOperation } from "@mrclrchtr/supi-tree-sitter/api";
3
5
  import type { StructuredFileAnalysis, StructuredPatternParams } from "./pattern-analysis.ts";
4
6
 
5
7
  interface ScanContext {
6
- readonly cwd: string;
7
8
  readonly params: StructuredPatternParams;
8
9
  }
9
10
 
@@ -26,8 +27,11 @@ export interface AstScanTimer {
26
27
  record(input: AstScanTimingInput): void;
27
28
  }
28
29
 
29
- /** Start one AST scan timer without adding paths to debug-event data. */
30
- export function startAstScanTimer(): AstScanTimer {
30
+ /** Start one AST scan timer, recording the workspace root as event-level cwd. */
31
+ export function startAstScanTimer(
32
+ control: CodeRequestControl | undefined,
33
+ cwd: string,
34
+ ): AstScanTimer {
31
35
  const timer = startDebugTimer();
32
36
  return {
33
37
  enumerationCompleted() {
@@ -36,11 +40,12 @@ export function startAstScanTimer(): AstScanTimer {
36
40
  record(input) {
37
41
  timer.finish(
38
42
  () => ({
43
+ operationId: control?.operationId,
39
44
  source: "code-intelligence",
40
45
  level: "debug",
41
46
  category: "ast-scan.timing",
42
47
  message: `AST ${input.context.params.kind} scan analyzed ${input.analysis.analyzedFileCount} files`,
43
- cwd: input.context.cwd,
48
+ cwd: truncateIdentity(cwd),
44
49
  data: {
45
50
  kind: input.context.params.kind,
46
51
  operation: input.operation,
@@ -292,7 +292,7 @@ class AstFileEnumerator {
292
292
  #checkControl(): boolean {
293
293
  this.options.signal?.throwIfAborted();
294
294
  if (this.#safetyLimited) return false;
295
- if (this.#now() <= this.options.deadline) return true;
295
+ if (this.#now() < this.options.deadline) return true;
296
296
  this.#markTimeout();
297
297
  return false;
298
298
  }
@@ -24,7 +24,7 @@ export async function settleByDeadline<T>(
24
24
  ): Promise<DeadlineOutcome<T>> {
25
25
  control.signal?.throwIfAborted();
26
26
  const remainingMs = control.deadline - control.now();
27
- if (remainingMs < 0) return { kind: "timeout" };
27
+ if (remainingMs <= 0) return { kind: "timeout" };
28
28
 
29
29
  return new Promise<DeadlineOutcome<T>>((resolve, reject) => {
30
30
  let settled = false;
@@ -70,7 +70,7 @@ export async function settleByDeadline<T>(
70
70
  .then(
71
71
  (value) => {
72
72
  complete(
73
- control.now() > control.deadline ? { kind: "timeout" } : { kind: "completed", value },
73
+ control.now() >= control.deadline ? { kind: "timeout" } : { kind: "completed", value },
74
74
  );
75
75
  },
76
76
  (error: unknown) => fail(error),