@mrclrchtr/supi-code-intelligence 4.10.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 (203) 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/settings/settings-registry.ts +3 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  28. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +71 -11
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +365 -297
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
  45. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  47. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  50. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
  52. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
  54. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  56. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  57. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  60. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  61. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
  62. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
  63. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
  64. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +17 -7
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +6 -3
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  122. package/package.json +5 -5
  123. package/src/analysis/health/diagnostics.ts +236 -24
  124. package/src/analysis/health/recovery.ts +6 -2
  125. package/src/analysis/provider.ts +58 -0
  126. package/src/analysis/readiness.ts +30 -9
  127. package/src/analysis/refactor/apply.ts +45 -1
  128. package/src/analysis/refactor/mutation-authority.ts +143 -0
  129. package/src/analysis/relations/callees.ts +9 -1
  130. package/src/analysis/relations/types.ts +5 -2
  131. package/src/analysis/search/ast-scan-timing.ts +9 -2
  132. package/src/analysis/search/ast-scan.ts +1 -1
  133. package/src/analysis/search/deadline.ts +2 -2
  134. package/src/analysis/search/pattern-analysis.ts +46 -32
  135. package/src/analysis/search/pattern.ts +12 -2
  136. package/src/analysis/target/anchored.ts +20 -11
  137. package/src/analysis/target/file.ts +18 -11
  138. package/src/analysis/target/identity.ts +19 -3
  139. package/src/analysis/target/symbol.ts +36 -15
  140. package/src/api.ts +4 -0
  141. package/src/diagnostics/evidence.ts +51 -0
  142. package/src/session/capability-adapter.ts +12 -3
  143. package/src/session/find-workflow.ts +17 -7
  144. package/src/session/graph/collect.ts +18 -2
  145. package/src/session/graph-workflow.ts +19 -12
  146. package/src/session/health-refresh.ts +148 -0
  147. package/src/session/health-types.ts +37 -15
  148. package/src/session/health-workflow.ts +37 -39
  149. package/src/session/inspect/collect.ts +20 -3
  150. package/src/session/inspect-workflow.ts +10 -5
  151. package/src/session/orientation/collect.ts +10 -1
  152. package/src/session/orientation/context-facts.ts +3 -1
  153. package/src/session/orientation/context-sections.ts +20 -7
  154. package/src/session/orientation/gather.ts +17 -11
  155. package/src/session/orientation-types.ts +2 -1
  156. package/src/session/orientation-workflow.ts +15 -8
  157. package/src/session/refactor-plans.ts +2 -0
  158. package/src/session/refactor-workflow.ts +59 -14
  159. package/src/session/session.ts +4 -1
  160. package/src/session/target-workflow.ts +54 -23
  161. package/src/session/workflow-control.ts +7 -3
  162. package/src/substrate/lsp/lifecycle.ts +26 -46
  163. package/src/substrate/lsp/maintenance.ts +199 -25
  164. package/src/substrate/lsp/state.ts +2 -2
  165. package/src/substrate/workspace-provider-host.ts +25 -3
  166. package/src/tool/find/execute.ts +7 -1
  167. package/src/tool/find/render.ts +20 -3
  168. package/src/tool/find/tui.ts +23 -21
  169. package/src/tool/graph/execute.ts +23 -3
  170. package/src/tool/graph/tui.ts +59 -46
  171. package/src/tool/health/execute.ts +7 -2
  172. package/src/tool/health/markdown.ts +56 -14
  173. package/src/tool/health/refresh-status.ts +105 -0
  174. package/src/tool/health/tui.ts +95 -98
  175. package/src/tool/infra/workflow-control.ts +8 -0
  176. package/src/tool/inspect/execute.ts +7 -1
  177. package/src/tool/inspect/tui.ts +26 -18
  178. package/src/tool/orientation/execute.ts +21 -3
  179. package/src/tool/orientation/tui.ts +51 -35
  180. package/src/tool/refactor-apply/execute.ts +3 -0
  181. package/src/tool/refactor-apply/tui.ts +43 -25
  182. package/src/tool/refactor-plan/execute.ts +4 -0
  183. package/src/tool/refactor-plan/tui.ts +33 -20
  184. package/src/tool/register.ts +127 -22
  185. package/src/tool/resolve/execute.ts +18 -1
  186. package/src/tool/resolve/tui.ts +42 -24
  187. package/src/tool/result/display.ts +107 -0
  188. package/src/tool/result/errors.ts +20 -17
  189. package/src/tool/result/find.ts +52 -8
  190. package/src/tool/result/graph.ts +44 -1
  191. package/src/tool/result/health.ts +53 -1
  192. package/src/tool/result/inspect.ts +113 -1
  193. package/src/tool/result/orientation.ts +66 -14
  194. package/src/tool/result/refactor.ts +36 -4
  195. package/src/tool/result/resolve.ts +44 -7
  196. package/src/tool/result/types.ts +20 -0
  197. package/src/types/execution.ts +18 -5
  198. package/src/types/index.ts +8 -1
  199. package/src/ui/status-command.ts +5 -3
  200. package/src/ui/status-overlay.ts +4 -0
  201. package/src/ui/tui/common.ts +108 -10
  202. package/src/ui/tui/display.ts +48 -0
  203. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -351
@@ -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,6 +16,7 @@ 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 { revalidateMutationAuthority } from "./mutation-authority.ts";
19
20
  import { compareCodePositions, createLogicalLineIndex } from "./position.ts";
20
21
  import { validateEditAgainstFiles } from "./safety.ts";
21
22
 
@@ -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>,
@@ -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
+ }
@@ -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,4 +1,6 @@
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
 
@@ -25,8 +27,11 @@ export interface AstScanTimer {
25
27
  record(input: AstScanTimingInput): void;
26
28
  }
27
29
 
28
- /** Start one AST scan timer without adding paths to debug-event data. */
29
- 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 {
30
35
  const timer = startDebugTimer();
31
36
  return {
32
37
  enumerationCompleted() {
@@ -35,10 +40,12 @@ export function startAstScanTimer(): AstScanTimer {
35
40
  record(input) {
36
41
  timer.finish(
37
42
  () => ({
43
+ operationId: control?.operationId,
38
44
  source: "code-intelligence",
39
45
  level: "debug",
40
46
  category: "ast-scan.timing",
41
47
  message: `AST ${input.context.params.kind} scan analyzed ${input.analysis.analyzedFileCount} files`,
48
+ cwd: truncateIdentity(cwd),
42
49
  data: {
43
50
  kind: input.context.params.kind,
44
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),
@@ -1,12 +1,15 @@
1
- import type {
2
- CodeResult,
3
- OutlineData,
4
- StructuralProvider as StructuralSubstrate,
1
+ import {
2
+ type CodeRequestControl,
3
+ type CodeResult,
4
+ isCodeRequestDeadlineError,
5
+ isCodeRequestInterruption,
6
+ type OutlineData,
7
+ type StructuralProvider as StructuralSubstrate,
5
8
  } from "@mrclrchtr/supi-code-runtime/api";
6
9
  import type { CodeFindAstKind } from "../../tool/find/ast-kinds.ts";
7
10
  import type { AstScanLimitation } from "./ast-scan.ts";
8
11
  import { callableExpressionForMatching } from "./call-name.ts";
9
- import { settleByDeadline } from "./deadline.ts";
12
+ import { type DeadlineOutcome, settleByDeadline } from "./deadline.ts";
10
13
  import { relativeDisplayPath } from "./paths.ts";
11
14
 
12
15
  export interface StructuredPatternParams {
@@ -52,6 +55,7 @@ interface AnalyzeStructuredFilesOptions {
52
55
  readonly deadline: number;
53
56
  readonly now: () => number;
54
57
  readonly signal?: AbortSignal;
58
+ readonly requestControl: CodeRequestControl;
55
59
  readonly initialLimitations: readonly AstScanLimitation[];
56
60
  }
57
61
 
@@ -67,34 +71,43 @@ export async function analyzeStructuredFiles(
67
71
 
68
72
  for (const [index, absoluteFile] of options.files.entries()) {
69
73
  options.signal?.throwIfAborted();
70
- if (options.now() > options.deadline) {
74
+ if (options.now() >= options.deadline) {
71
75
  addAnalysisTimeout(limitations, options.files.slice(index), options.displayBase);
72
76
  break;
73
77
  }
74
78
  const relativeFile = relativeDisplayPath(options.displayBase, absoluteFile);
75
79
  const fileMatches: StructuredMatch[] = [];
76
80
  const fileFailures: StructuredFailure[] = [];
77
- const outcome = await settleByDeadline(
78
- async () => {
79
- try {
80
- await collectMatchesForFile(
81
- fileMatches,
82
- fileFailures,
83
- options.structural,
84
- relativeFile,
85
- options.params.kind,
86
- matcher,
87
- );
88
- } catch (error) {
89
- fileFailures.push({
90
- file: relativeFile,
91
- kind: "runtime-error",
92
- reason: errorMessage(error),
93
- });
94
- }
95
- },
96
- { deadline: options.deadline, now: options.now, signal: options.signal },
97
- );
81
+ let outcome: DeadlineOutcome<void>;
82
+ try {
83
+ outcome = await settleByDeadline(
84
+ async () => {
85
+ try {
86
+ await collectMatchesForFile(
87
+ fileMatches,
88
+ fileFailures,
89
+ options.structural,
90
+ relativeFile,
91
+ options.params.kind,
92
+ matcher,
93
+ options.requestControl,
94
+ );
95
+ } catch (error) {
96
+ if (isCodeRequestInterruption(error, options.requestControl)) throw error;
97
+ fileFailures.push({
98
+ file: relativeFile,
99
+ kind: "runtime-error",
100
+ reason: errorMessage(error),
101
+ });
102
+ }
103
+ },
104
+ { deadline: options.deadline, now: options.now, signal: options.signal },
105
+ );
106
+ } catch (error) {
107
+ if (!isCodeRequestDeadlineError(error)) throw error;
108
+ addAnalysisTimeout(limitations, options.files.slice(index), options.displayBase);
109
+ break;
110
+ }
98
111
  if (outcome.kind === "timeout") {
99
112
  addAnalysisTimeout(limitations, options.files.slice(index), options.displayBase);
100
113
  break;
@@ -137,13 +150,14 @@ async function collectMatchesForFile(
137
150
  relFile: string,
138
151
  kind: CodeFindAstKind,
139
152
  matcher: (value: string) => boolean,
153
+ requestControl: CodeRequestControl,
140
154
  ): Promise<void> {
141
155
  const recordFailure = (kind: StructuredFailureKind, reason: string) => {
142
156
  failures.push({ file: relFile, kind, reason });
143
157
  };
144
158
 
145
159
  if (kind === "definition") {
146
- const outline = await structural.outline(relFile);
160
+ const outline = await structural.outline(relFile, requestControl);
147
161
  if (!handleStructuralResult(outline, recordFailure)) return;
148
162
  for (const item of flattenOutlineItems(outline.data)) {
149
163
  if (!matcher(item.name)) continue;
@@ -153,7 +167,7 @@ async function collectMatchesForFile(
153
167
  }
154
168
 
155
169
  if (kind === "export") {
156
- const exportsResult = await structural.exports(relFile);
170
+ const exportsResult = await structural.exports(relFile, requestControl);
157
171
  if (!handleStructuralResult(exportsResult, recordFailure)) return;
158
172
  for (const item of exportsResult.data) {
159
173
  if (!matcher(item.name)) continue;
@@ -163,7 +177,7 @@ async function collectMatchesForFile(
163
177
  }
164
178
 
165
179
  if (kind === "import") {
166
- const importsResult = await structural.imports(relFile);
180
+ const importsResult = await structural.imports(relFile, requestControl);
167
181
  if (!handleStructuralResult(importsResult, recordFailure)) return;
168
182
  for (const item of importsResult.data) {
169
183
  if (!matcher(item.moduleSpecifier)) continue;
@@ -178,7 +192,7 @@ async function collectMatchesForFile(
178
192
  }
179
193
 
180
194
  if (kind === "call") {
181
- const callResult = await structural.callSites(relFile);
195
+ const callResult = await structural.callSites(relFile, requestControl);
182
196
  if (!handleStructuralResult(callResult, recordFailure)) return;
183
197
  for (const call of callResult.data) {
184
198
  if (!matcher(callableExpressionForMatching(call.name))) continue;
@@ -187,7 +201,7 @@ async function collectMatchesForFile(
187
201
  return;
188
202
  }
189
203
 
190
- const outline = await structural.outline(relFile);
204
+ const outline = await structural.outline(relFile, requestControl);
191
205
  if (!handleStructuralResult(outline, recordFailure)) return;
192
206
  for (const item of flattenOutlineItems(outline.data)) {
193
207
  if (kind === "type" && !TYPE_KIND.test(item.kind.toLowerCase())) continue;
@@ -56,11 +56,14 @@ export type StructuredPatternOutcome =
56
56
 
57
57
  /** Internal controls for deterministic deadline, filesystem, and cancellation tests. */
58
58
  export interface StructuredPatternControl {
59
+ readonly operationId?: string;
59
60
  readonly signal?: AbortSignal;
60
61
  readonly now?: () => number;
61
62
  readonly operations?: AstScanOperations;
62
63
  readonly maxFiles?: number;
63
64
  readonly timeoutMs?: number;
65
+ /** Optional caller deadline; the earlier caller/policy deadline wins. */
66
+ readonly deadline?: number;
64
67
  }
65
68
 
66
69
  export interface StructuredPatternSearchOptions {
@@ -80,11 +83,12 @@ export interface StructuredPatternSearchOptions {
80
83
  export async function getStructuredPatternMatches(
81
84
  options: StructuredPatternSearchOptions,
82
85
  ): Promise<StructuredPatternOutcome> {
83
- const scanTimer = startAstScanTimer();
86
+ const scanTimer = startAstScanTimer(options.control, options.cwd);
84
87
  const now = options.control?.now ?? Date.now;
85
88
  const maxFiles = options.control?.maxFiles ?? DEFAULT_AST_SCAN_MAX_FILES;
86
89
  const timeoutMs = options.control?.timeoutMs ?? DEFAULT_AST_SCAN_TIMEOUT_MS;
87
- const deadline = now() + timeoutMs;
90
+ const policyDeadline = now() + timeoutMs;
91
+ const deadline = Math.min(options.control?.deadline ?? Number.POSITIVE_INFINITY, policyDeadline);
88
92
  const roots = Array.isArray(options.roots) ? [...options.roots] : [options.roots];
89
93
  const operation = structuralOperationForKind(options.params.kind);
90
94
  options.control?.signal?.throwIfAborted();
@@ -127,6 +131,11 @@ export async function getStructuredPatternMatches(
127
131
  }
128
132
 
129
133
  scanTimer.enumerationCompleted();
134
+ const requestControl = {
135
+ operationId: options.control?.operationId,
136
+ signal: options.control?.signal,
137
+ deadline,
138
+ };
130
139
  const analysis = await analyzeStructuredFiles({
131
140
  files: enumeration.files,
132
141
  displayBase: enumeration.displayBase,
@@ -135,6 +144,7 @@ export async function getStructuredPatternMatches(
135
144
  deadline,
136
145
  now,
137
146
  signal: options.control?.signal,
147
+ requestControl,
138
148
  initialLimitations: enumeration.limitations,
139
149
  });
140
150
  const capabilityMismatches = analysis.failures.filter(