@mrclrchtr/supi-code-intelligence 6.0.1 → 6.1.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 (59) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  4. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  5. package/node_modules/@mrclrchtr/supi-lsp/README.md +5 -1
  6. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-jsonrpc/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.colorProvider.d.ts +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.d.ts +2 -2
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.fileOperations.d.ts +9 -9
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/package.json +3 -3
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.d.ts +2 -2
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.js +1 -1
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.d.ts +2 -2
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.js +1 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/package.json +1 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  20. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +2 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +6 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +8 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +1 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
  25. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +582 -26
  26. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +2 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +10 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +36 -9
  29. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +2 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +4 -2
  31. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  32. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  33. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  34. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  37. package/package.json +5 -5
  38. package/src/analysis/health/recovery.ts +4 -0
  39. package/src/analysis/health/server-status.ts +64 -0
  40. package/src/analysis/health/signals.ts +1 -0
  41. package/src/analysis/provider.ts +2 -1
  42. package/src/analysis/target/symbol.ts +28 -16
  43. package/src/analysis/target/types.ts +1 -0
  44. package/src/session/find-workflow.ts +5 -12
  45. package/src/session/health-refresh.ts +6 -2
  46. package/src/session/health-types.ts +8 -2
  47. package/src/session/health-workflow.ts +5 -1
  48. package/src/session/semantic-demand.ts +36 -0
  49. package/src/session/target-workflow.ts +9 -12
  50. package/src/tool/code_health/guidance.ts +2 -2
  51. package/src/tool/code_health/markdown.ts +15 -3
  52. package/src/tool/code_health/result.ts +14 -5
  53. package/src/tool/code_health/spec.ts +2 -1
  54. package/src/tool/code_health/tui.ts +24 -3
  55. package/src/tool/result/errors.ts +1 -0
  56. package/src/tool/result/types.ts +6 -0
  57. package/src/ui/footer.ts +7 -5
  58. package/src/ui/status-command.ts +31 -0
  59. package/src/ui/status-overlay.ts +69 -23
@@ -92,10 +92,9 @@ export function createLspSemanticProvider(lsp: WorkspaceLspRuntime): SemanticPro
92
92
  async workspaceSymbols(
93
93
  query: string,
94
94
  control?: CodeRequestControl,
95
+ scopes?: readonly string[],
95
96
  ): Promise<CodeQueryResult<CodeSymbol[]>> {
96
- const result = control
97
- ? await lsp.workspaceSymbol(query, control)
98
- : await lsp.workspaceSymbol(query);
97
+ const result = await lsp.workspaceSymbol(query, control, scopes);
99
98
  return mapCodeQueryResult(result, (results) =>
100
99
  results.map((symbol) => toCodeSymbol(symbol as SymbolInformation)),
101
100
  );
@@ -8,6 +8,14 @@ import type {
8
8
  WorkspaceDiagnosticSummaryEntry,
9
9
  } from "./runtime-diagnostics.ts";
10
10
 
11
+ /**
12
+ * Explicit diagnostic demand that can recover previously running crashed
13
+ * routes. Omitted scopes include every route with retained tracked files.
14
+ */
15
+ export interface ProcessCrashDiagnosticDemand {
16
+ readonly scopes?: readonly string[];
17
+ }
18
+
11
19
  /** Diagnostic and recovery operations owned by a workspace LSP runtime. */
12
20
  export interface WorkspaceLspDiagnosticSurface {
13
21
  fileDiagnostics(
@@ -37,6 +45,8 @@ export interface WorkspaceLspDiagnosticSurface {
37
45
  quietMs?: number;
38
46
  /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
39
47
  initialEvidence?: DiagnosticEvidenceSummary;
48
+ /** Explicit demand to recover crashed routes with tracked files in scope. */
49
+ processCrashDemand?: ProcessCrashDiagnosticDemand;
40
50
  control?: CodeRequestControl;
41
51
  }): Promise<RecoverDiagnosticsResult>;
42
52
  }
@@ -38,7 +38,10 @@ import type {
38
38
  WorkspaceLspRuntimeState,
39
39
  } from "./workspace-lsp-runtime.ts";
40
40
 
41
- export type { WorkspaceLspDiagnosticSurface } from "./runtime-diagnostic-surface.ts";
41
+ export type {
42
+ ProcessCrashDiagnosticDemand,
43
+ WorkspaceLspDiagnosticSurface,
44
+ } from "./runtime-diagnostic-surface.ts";
42
45
  export type {
43
46
  DiagnosticEvidenceDocument,
44
47
  DiagnosticEvidenceStatus,
@@ -107,7 +110,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
107
110
  control?: CodeRequestControl,
108
111
  ): Promise<CodeQueryResult<Hover | null>> {
109
112
  const resolvedPath = this.resolveFilePath(filePath);
110
- const client = await this.manager.ensureFileOpen(resolvedPath);
113
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
114
+ recoverProcessCrash: true,
115
+ control,
116
+ });
111
117
  if (!client) return unavailableFileQuery("hover", resolvedPath);
112
118
  return client.hover(resolvedPath, position, control);
113
119
  }
@@ -118,7 +124,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
118
124
  control?: CodeRequestControl,
119
125
  ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
120
126
  const resolvedPath = this.resolveFilePath(filePath);
121
- const client = await this.manager.ensureFileOpen(resolvedPath);
127
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
128
+ recoverProcessCrash: true,
129
+ control,
130
+ });
122
131
  if (!client) return unavailableFileQuery("definition", resolvedPath);
123
132
  return client.definition(resolvedPath, position, control);
124
133
  }
@@ -129,7 +138,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
129
138
  control?: CodeRequestControl,
130
139
  ): Promise<CodeQueryResult<Location[]>> {
131
140
  const resolvedPath = this.resolveFilePath(filePath);
132
- const client = await this.manager.ensureFileOpen(resolvedPath);
141
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
142
+ recoverProcessCrash: true,
143
+ control,
144
+ });
133
145
  if (!client) return unavailableFileQuery("references", resolvedPath);
134
146
  return mapCodeQueryResult(
135
147
  await client.references(resolvedPath, position, control),
@@ -143,7 +155,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
143
155
  control?: CodeRequestControl,
144
156
  ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
145
157
  const resolvedPath = this.resolveFilePath(filePath);
146
- const client = await this.manager.ensureFileOpen(resolvedPath);
158
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
159
+ recoverProcessCrash: true,
160
+ control,
161
+ });
147
162
  if (!client) return unavailableFileQuery("implementation", resolvedPath);
148
163
  return client.implementation(resolvedPath, position, control);
149
164
  }
@@ -153,7 +168,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
153
168
  control?: CodeRequestControl,
154
169
  ): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[]>> {
155
170
  const resolvedPath = this.resolveFilePath(filePath);
156
- const client = await this.manager.ensureFileOpen(resolvedPath);
171
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
172
+ recoverProcessCrash: true,
173
+ control,
174
+ });
157
175
  if (!client) return unavailableFileQuery("document symbols", resolvedPath);
158
176
  return mapCodeQueryResult(
159
177
  await client.documentSymbols(resolvedPath, control),
@@ -164,8 +182,9 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
164
182
  async workspaceSymbol(
165
183
  query: string,
166
184
  control?: CodeRequestControl,
185
+ scopes?: readonly string[],
167
186
  ): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[]>> {
168
- return this.manager.workspaceSymbol(query, control);
187
+ return this.manager.workspaceSymbol(query, control, scopes);
169
188
  }
170
189
 
171
190
  async rename(
@@ -175,7 +194,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
175
194
  control?: CodeRequestControl,
176
195
  ): Promise<RoutedMutationResponse<WorkspaceEdit | null> | null> {
177
196
  const resolvedPath = this.resolveFilePath(filePath);
178
- const client = await this.manager.ensureFileOpen(resolvedPath);
197
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
198
+ recoverProcessCrash: true,
199
+ control,
200
+ });
179
201
  if (!client) return null;
180
202
  return {
181
203
  value: await client.rename(resolvedPath, position, newName, control),
@@ -193,7 +215,10 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
193
215
  control?: CodeRequestControl,
194
216
  ): Promise<RoutedMutationResponse<CodeAction[] | null> | null> {
195
217
  const resolvedPath = this.resolveFilePath(filePath);
196
- const client = await this.manager.ensureFileOpen(resolvedPath);
218
+ const client = await this.manager.ensureFileOpen(resolvedPath, {
219
+ recoverProcessCrash: true,
220
+ control,
221
+ });
197
222
  if (!client) return null;
198
223
 
199
224
  const range = isRange(positionOrRange)
@@ -345,6 +370,8 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
345
370
  quietMs?: number;
346
371
  /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
347
372
  initialEvidence?: DiagnosticEvidenceSummary;
373
+ /** Explicit demand to recover crashed routes with tracked files in scope. */
374
+ processCrashDemand?: import("./runtime-diagnostic-surface.ts").ProcessCrashDiagnosticDemand;
348
375
  control?: CodeRequestControl;
349
376
  }): Promise<RecoverDiagnosticsResult> {
350
377
  const recoveryStartedAt = Date.now();
@@ -8,6 +8,7 @@ export interface TransitionServerEntry {
8
8
  readonly name: string;
9
9
  readonly status: ProjectServerInfo["status"];
10
10
  readonly ready: boolean;
11
+ readonly statusReason?: NonNullable<ProjectServerInfo["statusReason"]>;
11
12
  }
12
13
 
13
14
  /** Bound a project-server snapshot to MAX_SERVERS name/status/ready entries. */
@@ -18,6 +19,7 @@ function boundedServerEntries(
18
19
  name: truncateIdentity(server.name),
19
20
  status: server.status,
20
21
  ready: server.ready,
22
+ ...(server.statusReason ? { statusReason: server.statusReason } : {}),
21
23
  }));
22
24
  }
23
25
 
@@ -40,8 +40,8 @@ export interface RoutedMutationResponse<T> {
40
40
  * diagnostics, and recovery without exposing clients or the mutable manager.
41
41
  * File paths can be absolute or session-cwd-relative. A leading `@` is removed to
42
42
  * match Pi's built-in path-tool convention. Positions use raw 0-based LSP coordinates.
43
- * Provider request control is accepted
44
- * as metadata only in this expansion and is not interpreted by the runtime.
43
+ * Request control is forwarded to the routed client and limits the caller's
44
+ * wait without cancelling shared route recovery.
45
45
  */
46
46
  export interface WorkspaceLspRuntime extends WorkspaceLspDiagnosticSurface {
47
47
  hover(
@@ -68,9 +68,11 @@ export interface WorkspaceLspRuntime extends WorkspaceLspDiagnosticSurface {
68
68
  filePath: string,
69
69
  control?: CodeRequestControl,
70
70
  ): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[]>>;
71
+ /** Collect symbols from routes that can contribute within the optional path scopes. */
71
72
  workspaceSymbol(
72
73
  query: string,
73
74
  control?: CodeRequestControl,
75
+ scopes?: readonly string[],
74
76
  ): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[]>>;
75
77
  rename(
76
78
  filePath: string,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -75,9 +75,14 @@ export interface SemanticProvider {
75
75
  filePath: string,
76
76
  control?: CodeRequestControl,
77
77
  ): Promise<CodeQueryResult<DocumentCodeSymbol[]>>;
78
+ /**
79
+ * Collect workspace symbols from routes that can contribute within the
80
+ * optional canonical path scopes.
81
+ */
78
82
  workspaceSymbols(
79
83
  query: string,
80
84
  control?: CodeRequestControl,
85
+ scopes?: readonly string[],
81
86
  ): Promise<CodeQueryResult<CodeSymbol[]>>;
82
87
 
83
88
  /** Optional definition capability with explicit completed-empty semantics. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "Structural AST analysis for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,8 +36,8 @@
36
36
  "dependencies": {
37
37
  "jiti": "^2.7.0",
38
38
  "web-tree-sitter": "^0.26.8",
39
- "@mrclrchtr/supi-code-runtime": "6.0.1",
40
- "@mrclrchtr/supi-core": "6.0.1"
39
+ "@mrclrchtr/supi-code-runtime": "6.1.0",
40
+ "@mrclrchtr/supi-core": "6.1.0"
41
41
  },
42
42
  "bundledDependencies": [
43
43
  "@mrclrchtr/supi-code-runtime",
@@ -6,7 +6,7 @@
6
6
  "releaseAsset": "https://github.com/fwcd/tree-sitter-kotlin/releases/download/0.3.8/tree-sitter-kotlin.wasm"
7
7
  },
8
8
  "generatedWith": {
9
- "treeSitterCli": "0.26.12"
9
+ "treeSitterCli": "0.26.13"
10
10
  },
11
11
  "sha256": "e08447e36ad5d86f39f91f13defce0636aea1490b8d65dafde6fa72649422061"
12
12
  }
@@ -5,7 +5,7 @@
5
5
  "repository": "https://github.com/derekstride/tree-sitter-sql"
6
6
  },
7
7
  "generatedWith": {
8
- "treeSitterCli": "0.26.12"
8
+ "treeSitterCli": "0.26.13"
9
9
  },
10
10
  "sha256": "9b93a15dc3eade66df4347731f2687b939daf987c8382660d6118e863604ab09",
11
11
  "trust": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-intelligence",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "LSP and AST navigation, search, diagnostics, and refactoring",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "yaml": "^2.9.0",
38
- "@mrclrchtr/supi-code-runtime": "6.0.1",
39
- "@mrclrchtr/supi-tree-sitter": "6.0.1",
40
- "@mrclrchtr/supi-lsp": "6.0.1",
41
- "@mrclrchtr/supi-core": "6.0.1"
38
+ "@mrclrchtr/supi-lsp": "6.1.0",
39
+ "@mrclrchtr/supi-core": "6.1.0",
40
+ "@mrclrchtr/supi-code-runtime": "6.1.0",
41
+ "@mrclrchtr/supi-tree-sitter": "6.1.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",
@@ -4,6 +4,7 @@
4
4
  import type { CapabilityState, CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
5
5
  import type {
6
6
  DiagnosticEvidenceSummary,
7
+ ProcessCrashDiagnosticDemand,
7
8
  RecoverDiagnosticsResult,
8
9
  WorkspaceLspRuntime,
9
10
  } from "@mrclrchtr/supi-lsp/api";
@@ -16,6 +17,8 @@ interface RecoverOptions {
16
17
  control?: CodeRequestControl;
17
18
  /** Evidence from the maintenance refresh, so the recovery pass skips its own refresh. */
18
19
  initialEvidence?: DiagnosticEvidenceSummary;
20
+ /** Explicit demand for crashed diagnostic routes before diagnostic recovery. */
21
+ processCrashDemand?: ProcessCrashDiagnosticDemand;
19
22
  }
20
23
 
21
24
  /** Run the runtime's best-effort recovery and preserve its established outcome. */
@@ -26,6 +29,7 @@ export async function recoverDiagnosticRuntime(
26
29
  return opts.service.recoverDiagnostics({
27
30
  restartIfStillStale: true,
28
31
  ...(opts.initialEvidence !== undefined ? { initialEvidence: opts.initialEvidence } : {}),
32
+ ...(opts.processCrashDemand ? { processCrashDemand: opts.processCrashDemand } : {}),
29
33
  control: opts.control,
30
34
  });
31
35
  }
@@ -0,0 +1,64 @@
1
+ import * as path from "node:path";
2
+ import type { ProjectServerInfo, ProjectServerStatusReason } from "@mrclrchtr/supi-lsp/api";
3
+
4
+ /** Workspace-wide counts for non-running LSP routes. */
5
+ export interface ProjectServerRouteStatusCounts {
6
+ recovering: number;
7
+ error: number;
8
+ unavailable: number;
9
+ }
10
+
11
+ /** Count typed route states without changing semantic readiness. */
12
+ export function countProjectServerRouteStatuses(
13
+ servers: readonly Pick<ProjectServerInfo, "status" | "statusReason">[],
14
+ ): ProjectServerRouteStatusCounts {
15
+ return {
16
+ recovering: servers.filter((server) => server.statusReason === "process-crash-recovery-pending")
17
+ .length,
18
+ error: servers.filter(
19
+ (server) =>
20
+ server.status === "error" && server.statusReason !== "process-crash-recovery-pending",
21
+ ).length,
22
+ unavailable: servers.filter((server) => server.status === "unavailable").length,
23
+ };
24
+ }
25
+
26
+ /** Render nonzero workspace route counts for an aggregate status surface. */
27
+ export function formatProjectServerRouteSummary(
28
+ servers: readonly Pick<ProjectServerInfo, "status" | "statusReason">[],
29
+ ): string | null {
30
+ return formatProjectServerRouteStatusCounts(countProjectServerRouteStatuses(servers));
31
+ }
32
+
33
+ /** Render one structured workspace route-count value. */
34
+ export function formatProjectServerRouteStatusCounts(
35
+ counts: ProjectServerRouteStatusCounts,
36
+ ): string | null {
37
+ const parts: string[] = [];
38
+ if (counts.recovering > 0) parts.push(`${counts.recovering} recovering`);
39
+ if (counts.error > 0) parts.push(`${counts.error} error${counts.error === 1 ? "" : "s"}`);
40
+ if (counts.unavailable > 0) parts.push(`${counts.unavailable} unavailable`);
41
+ return parts.length > 0 ? `workspace routes: ${parts.join(", ")}` : null;
42
+ }
43
+
44
+ /** Render an LSP route root relative to its workspace. */
45
+ export function formatProjectServerRoot(cwd: string, root: string): string {
46
+ const relative = path.relative(cwd, root);
47
+ return (relative || ".").replaceAll("\\", "/");
48
+ }
49
+
50
+ /** Render a concise explanation for a process-crash recovery state. */
51
+ export function formatProjectServerStatusReason(
52
+ reason: ProjectServerStatusReason | undefined,
53
+ ): string | null {
54
+ switch (reason) {
55
+ case "process-crashed":
56
+ return "process crashed; next evidence operation will recover";
57
+ case "process-crash-recovery-pending":
58
+ return "process recovery in progress";
59
+ case "process-crash-recovery-exhausted":
60
+ return "process recovery exhausted; reload required";
61
+ default:
62
+ return null;
63
+ }
64
+ }
@@ -15,6 +15,7 @@ export function collectServers(
15
15
  root: server.root,
16
16
  fileTypes: server.fileTypes,
17
17
  status: server.status,
18
+ ...(server.statusReason ? { statusReason: server.statusReason } : {}),
18
19
  ready: server.ready,
19
20
  }));
20
21
  }
@@ -97,7 +97,8 @@ export function withSemanticRequestControl<T extends SemanticProvider>(
97
97
  references: (file, position) => provider.references(file, position, control),
98
98
  implementation: (file, position) => provider.implementation(file, position, control),
99
99
  documentSymbols: (file) => provider.documentSymbols(file, control),
100
- workspaceSymbols: (query) => provider.workspaceSymbols(query, control),
100
+ workspaceSymbols: (query, _requestControl, scopes) =>
101
+ provider.workspaceSymbols(query, control, scopes),
101
102
  hover: provider.hover
102
103
  ? (file, position) => provider.hover?.(file, position, control)
103
104
  : undefined,
@@ -8,6 +8,7 @@
8
8
 
9
9
  import * as path from "node:path";
10
10
  import {
11
+ type CodeQueryResult,
11
12
  type CodeRequestControl,
12
13
  type CodeSymbol,
13
14
  isCodeRequestInterruption,
@@ -114,33 +115,27 @@ export async function resolveSymbolTarget(
114
115
  control?: CodeRequestControl;
115
116
  },
116
117
  ): Promise<TargetOutcome> {
117
- const result = await semantic.workspaceSymbols(symbol);
118
+ const scopePath = options?.path ? normalizePath(options.path, cwd) : null;
119
+ const result = await semantic.workspaceSymbols(
120
+ symbol,
121
+ options?.control,
122
+ scopePath ? [scopePath] : undefined,
123
+ );
118
124
  if (result.kind === "unavailable") {
119
125
  return {
120
- kind: "error",
121
- message: `Symbol discovery for \`${symbol}\` is unavailable: ${result.reason}`,
122
- };
123
- }
124
- if (result.data.length === 0) {
125
- return {
126
- kind: "error",
127
- message: `Symbol not found: \`${symbol}\`. Try \`code_resolve\` to search for it, or use an anchor target (file/line/character) if you know the location.`,
126
+ kind: "unavailable",
127
+ reason: `Symbol discovery for \`${symbol}\` is unavailable: ${result.reason}`,
128
128
  };
129
129
  }
130
+ if (result.data.length === 0) return emptySymbolOutcome(symbol, result, options?.path);
130
131
 
131
- const scopePath = options?.path ? normalizePath(options.path, cwd) : null;
132
132
  const scoped = result.data.filter((candidate) =>
133
133
  scopePath ? isWithinOrEqual(scopePath, candidate.file) : true,
134
134
  );
135
135
  const eligible = options?.exportedOnly
136
136
  ? scoped.filter((candidate) => !NON_EXPORTED_KINDS.has(candidate.kind))
137
137
  : scoped;
138
- if (eligible.length === 0) {
139
- return {
140
- kind: "error",
141
- message: `Symbol not found: \`${symbol}\`${scopePath ? ` in path \`${options?.path}\`` : ""}. Try \`code_resolve\` to search for it, or use an anchor target (file/line/character) if you know the location.`,
142
- };
143
- }
138
+ if (eligible.length === 0) return emptySymbolOutcome(symbol, result, options?.path);
144
139
 
145
140
  const requestedKind = options?.kind;
146
141
  if (requestedKind) {
@@ -248,6 +243,23 @@ type CandidateOutcomeOptions = CandidateOutcomeBase &
248
243
  * exact provider result count for omission disclosure. No candidate is
249
244
  * promoted when the requested provider kind did not match.
250
245
  */
246
+ function emptySymbolOutcome(
247
+ symbol: string,
248
+ result: Exclude<CodeQueryResult<CodeSymbol[]>, { kind: "unavailable" }>,
249
+ scope?: string,
250
+ ): TargetOutcome {
251
+ if (result.kind === "partial") {
252
+ return {
253
+ kind: "unavailable",
254
+ reason: `Symbol discovery for \`${symbol}\` is incomplete: ${result.reason}`,
255
+ };
256
+ }
257
+ return {
258
+ kind: "error",
259
+ message: `Symbol not found: \`${symbol}\`${scope ? ` in path \`${scope}\`` : ""}. Try \`code_resolve\` to search for it, or use an anchor target (file/line/character) if you know the location.`,
260
+ };
261
+ }
262
+
251
263
  async function buildCandidateOutcome(
252
264
  options: CandidateOutcomeOptions,
253
265
  ): Promise<Extract<TargetOutcome, { kind: "disambiguation" | "kind-mismatch" }>> {
@@ -156,4 +156,5 @@ export type TargetOutcome =
156
156
  candidates: DisambiguationCandidateData[];
157
157
  omittedCount: number;
158
158
  }
159
+ | { kind: "unavailable"; reason: string }
159
160
  | { kind: "error"; message: string };
@@ -9,6 +9,7 @@ import { isCodeFindAstKind } from "../tool/code_find/ast-kinds.ts";
9
9
  import type { CapabilityAdapter } from "./capability-adapter.ts";
10
10
  import type { FindWorkflowInput, FindWorkflowOutcome } from "./find-types.ts";
11
11
  import { parseFindWorkflowInput } from "./input/workflows.ts";
12
+ import { resolveWorkspaceSemanticDemand } from "./semantic-demand.ts";
12
13
  import { reportProgress, throwIfAborted, type WorkflowControl } from "./workflow-control.ts";
13
14
 
14
15
  export interface FindWorkflowDeps {
@@ -71,22 +72,14 @@ async function runSemanticSearch(options: {
71
72
  control?: WorkflowControl;
72
73
  }): Promise<FindWorkflowOutcome> {
73
74
  const { query, scopePaths, scopeLabel, maxResults, deps, control } = options;
74
- const readiness = await deps.capability.ensureSemanticReadiness(
75
- deps.cwd,
76
- { kind: "workspace" },
77
- control,
78
- );
79
- if (readiness.kind === "timeout") {
75
+ const semanticDemand = await resolveWorkspaceSemanticDemand(deps.capability, deps.cwd, control);
76
+ if (semanticDemand.kind === "timeout") {
80
77
  return { kind: "unavailable", reason: SEMANTIC_READINESS_TIMEOUT_REASON };
81
78
  }
82
- if (readiness.kind === "unavailable") return readiness;
79
+ if (semanticDemand.kind === "unavailable") return semanticDemand;
83
80
  throwIfAborted(control);
84
81
 
85
- const provider = deps.capability.getSemanticProvider(deps.cwd);
86
- if (!provider?.workspaceSymbols) {
87
- return { kind: "unavailable", reason: "No semantic workspace-symbol provider is active." };
88
- }
89
- const result = await provider.workspaceSymbols(query, control);
82
+ const result = await semanticDemand.provider.workspaceSymbols(query, control, scopePaths);
90
83
  // A cancellation that landed during the request must not publish symbols
91
84
  // the caller no longer awaits.
92
85
  throwIfAborted(control);
@@ -105,9 +105,13 @@ async function collectWorkspaceRefreshAttempt(
105
105
  service: options.runtime,
106
106
  control: options.control,
107
107
  progress: options.reportRecoveryProgress,
108
- // The maintenance pass already refreshed every open document; reuse its
109
- // evidence so the recovery pass skips a redundant second refresh.
108
+ // The maintenance pass already refreshed every running route. Reuse its
109
+ // evidence unless process-crash demand restores a required route; the
110
+ // runtime then performs a fresh pass that includes the replacement.
110
111
  initialEvidence: maintenance.diagnosticEvidence,
112
+ processCrashDemand: {
113
+ ...(workspaceScope?.filter ? { scopes: [workspaceScope.filter] } : {}),
114
+ },
111
115
  });
112
116
  const diagnosticEvidence = mergeDiagnosticEvidence(
113
117
  maintenance.diagnosticEvidence,
@@ -1,4 +1,9 @@
1
- import type { DiagnosticEvidenceSummary, FileScopeDecision } from "@mrclrchtr/supi-lsp/api";
1
+ import type {
2
+ DiagnosticEvidenceSummary,
3
+ FileScopeDecision,
4
+ ProjectServerInfo,
5
+ ProjectServerStatusReason,
6
+ } from "@mrclrchtr/supi-lsp/api";
2
7
  import type { CapabilityWarningReport } from "../analysis/capability/capability-warnings.ts";
3
8
 
4
9
  export type HealthSection = "diagnostics" | "servers";
@@ -22,7 +27,8 @@ export interface HealthServerInfo {
22
27
  readonly name: string;
23
28
  readonly root: string;
24
29
  readonly fileTypes: readonly string[];
25
- readonly status: string;
30
+ readonly status: ProjectServerInfo["status"];
31
+ readonly statusReason?: ProjectServerStatusReason;
26
32
  readonly ready: boolean;
27
33
  }
28
34
 
@@ -59,6 +59,7 @@ export async function runHealthWorkflow(
59
59
  if (prepared.kind === "invalid-input") return prepared;
60
60
  const { request, scopeFilter, included, level } = prepared.value;
61
61
  const semanticRequested = healthNeedsSemantic(included);
62
+ const fileReadinessRequested = included.includes("diagnostics");
62
63
  throwIfAborted(control);
63
64
 
64
65
  reportProgress(control, {
@@ -85,6 +86,7 @@ export async function runHealthWorkflow(
85
86
  });
86
87
  const semanticState = await establishSemanticHealthState({
87
88
  requested: semanticRequested,
89
+ fileReadinessRequested,
88
90
  runtime,
89
91
  scopeFilter,
90
92
  lspState,
@@ -139,6 +141,8 @@ function healthNeedsSemantic(included: readonly HealthSection[]): boolean {
139
141
 
140
142
  interface SemanticHealthStateOptions {
141
143
  requested: boolean;
144
+ /** Whether this call requests file-routed evidence rather than passive inventory. */
145
+ fileReadinessRequested: boolean;
142
146
  runtime: WorkspaceLspRuntime | null;
143
147
  scopeFilter: string | null;
144
148
  lspState: WorkspaceLspRuntimeState;
@@ -150,7 +154,7 @@ async function establishSemanticHealthState(
150
154
  options: SemanticHealthStateOptions,
151
155
  ): Promise<SemanticHealthState | null> {
152
156
  if (!options.requested) return null;
153
- if (options.runtime && isScopedFile(options.scopeFilter)) {
157
+ if (options.fileReadinessRequested && options.runtime && isScopedFile(options.scopeFilter)) {
154
158
  const readiness = await options.runtime.waitUntilReadyForFile(
155
159
  options.scopeFilter,
156
160
  undefined,
@@ -0,0 +1,36 @@
1
+ import type { CodeRequestControl, SemanticProvider } from "@mrclrchtr/supi-code-runtime/api";
2
+ import type { CapabilityAdapter, ReadinessOutcome } from "./capability-adapter.ts";
3
+
4
+ export type WorkspaceSemanticDemandState =
5
+ | { kind: "ready"; provider: SemanticProvider }
6
+ | Exclude<ReadinessOutcome, { kind: "ready" }>;
7
+
8
+ /**
9
+ * Wait for normal workspace startup, but let a registered provider proceed
10
+ * when known crashed routes make readiness recoverable only through demand.
11
+ */
12
+ export async function resolveWorkspaceSemanticDemand(
13
+ capability: CapabilityAdapter,
14
+ cwd: string,
15
+ control?: CodeRequestControl,
16
+ ): Promise<WorkspaceSemanticDemandState> {
17
+ let provider = capability.getSemanticProvider(cwd);
18
+ const readiness = await capability.ensureSemanticReadiness(cwd, { kind: "workspace" }, control);
19
+ if (readiness.kind === "timeout") return readiness;
20
+ if (readiness.kind === "unavailable" && !hasProcessCrashRoute(capability, cwd)) {
21
+ return readiness;
22
+ }
23
+
24
+ provider ??= capability.getSemanticProvider(cwd);
25
+ return provider
26
+ ? { kind: "ready", provider }
27
+ : { kind: "unavailable", reason: "No semantic workspace-symbol provider is active." };
28
+ }
29
+
30
+ function hasProcessCrashRoute(capability: CapabilityAdapter, cwd: string): boolean {
31
+ const lspState = capability.getLspRuntimeState(cwd);
32
+ return (
33
+ lspState.kind === "ready" &&
34
+ lspState.runtime.getProjectServers().some((server) => server.statusReason !== undefined)
35
+ );
36
+ }