@mrclrchtr/supi-code-intelligence 6.1.0 → 6.2.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 (114) 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-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  6. package/node_modules/@mrclrchtr/supi-lsp/README.md +21 -3
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  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/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  11. package/node_modules/@mrclrchtr/supi-lsp/package.json +4 -5
  12. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -7
  13. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +0 -5
  15. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +6 -3
  16. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +2 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +5 -2
  18. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +2 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +17 -5
  20. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +40 -18
  21. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +164 -30
  22. package/node_modules/@mrclrchtr/supi-lsp/src/config/lsp-settings.ts +9 -87
  23. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +0 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
  25. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +0 -29
  26. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +7 -21
  27. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-severity.ts +6 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +11 -14
  29. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +0 -5
  30. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +0 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +29 -49
  32. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +1 -1
  33. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -8
  34. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +1 -9
  35. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
  36. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +9 -1
  37. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +26 -41
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +275 -99
  39. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +4 -1
  40. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +59 -28
  41. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +27 -19
  42. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +26 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registration.ts +0 -17
  44. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +83 -19
  45. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +5 -2
  46. package/node_modules/@mrclrchtr/supi-lsp/src/session/scanner.ts +24 -21
  47. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +29 -3
  48. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +19 -40
  49. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +13 -19
  50. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +287 -0
  51. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +6 -18
  52. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  53. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  54. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  55. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -5
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -11
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +19 -0
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +5 -23
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +3 -21
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +5 -9
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +10 -20
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +7 -1
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +9 -5
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +134 -9
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-name.ts +96 -0
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +11 -12
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +39 -56
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +9 -7
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/js-binding-pattern.ts +26 -0
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +2 -21
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +144 -12
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +50 -37
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/scope.ts +138 -0
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +0 -7
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +1 -14
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +0 -10
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +6 -8
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +34 -10
  80. package/package.json +5 -5
  81. package/src/analysis/capability/capability-warnings.ts +4 -22
  82. package/src/analysis/search/ast-scan-timing.ts +4 -2
  83. package/src/analysis/search/ast-scan.ts +1 -1
  84. package/src/api.ts +1 -0
  85. package/src/extension.ts +4 -2
  86. package/src/session/health-refresh.ts +24 -2
  87. package/src/session/health-types.ts +19 -0
  88. package/src/session/input/health-refactor.ts +16 -14
  89. package/src/session/refactor-types.ts +14 -1
  90. package/src/session/refactor-workflow.ts +21 -16
  91. package/src/substrate/lsp/lifecycle.ts +0 -3
  92. package/src/substrate/lsp/maintenance.ts +4 -5
  93. package/src/substrate/lsp/settings.ts +6 -9
  94. package/src/substrate/lsp/state.ts +0 -2
  95. package/src/substrate/workspace-provider-host.ts +2 -2
  96. package/src/tool/code_health/file-scope-markdown.ts +43 -0
  97. package/src/tool/code_health/markdown.ts +66 -66
  98. package/src/tool/code_health/refresh-outcome.ts +46 -0
  99. package/src/tool/code_health/refresh-status.ts +166 -23
  100. package/src/tool/code_health/result.ts +1 -0
  101. package/src/tool/code_health/tui.ts +18 -5
  102. package/src/tool/code_refactor_apply/guidance.ts +3 -2
  103. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  104. package/src/tool/register.ts +5 -2
  105. package/src/tool/schemas.ts +21 -11
  106. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-actions.ts +0 -59
  107. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +0 -24
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +0 -132
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +0 -158
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +0 -167
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -42
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-registration.ts +0 -37
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +0 -30
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/structure.ts +0 -8
@@ -96,7 +96,10 @@ export function createLspSemanticProvider(lsp: WorkspaceLspRuntime): SemanticPro
96
96
  ): Promise<CodeQueryResult<CodeSymbol[]>> {
97
97
  const result = await lsp.workspaceSymbol(query, control, scopes);
98
98
  return mapCodeQueryResult(result, (results) =>
99
- results.map((symbol) => toCodeSymbol(symbol as SymbolInformation)),
99
+ results.flatMap((symbol) => {
100
+ const mapped = toCodeSymbol(symbol as SymbolInformation);
101
+ return mapped ? [mapped] : [];
102
+ }),
100
103
  );
101
104
  },
102
105
 
@@ -30,20 +30,34 @@ function toCodeLocation(item: Location | LocationLink): CodeLocation | null {
30
30
  const loc = item as Record<string, unknown>;
31
31
  const uri = loc.uri ?? loc.targetUri;
32
32
  if (typeof uri !== "string") return null;
33
- const range = loc.targetSelectionRange ?? loc.targetRange ?? loc.range;
34
- if (!range || typeof range !== "object") return null;
35
- const r = range as { start: Record<string, unknown>; end: Record<string, unknown> };
36
- if (!r.start || !r.end) return null;
37
- return {
38
- uri,
39
- range: {
40
- start: {
41
- line: (r.start.line as number) ?? 0,
42
- character: (r.start.character as number) ?? 0,
43
- },
44
- end: { line: (r.end.line as number) ?? 0, character: (r.end.character as number) ?? 0 },
45
- },
46
- };
33
+ const range = toValidRange(loc.targetSelectionRange ?? loc.targetRange ?? loc.range);
34
+ return range ? { uri, range } : null;
35
+ }
36
+
37
+ function toValidRange(value: unknown): SourceRange | null {
38
+ if (!value || typeof value !== "object") return null;
39
+ const range = value as { start?: unknown; end?: unknown };
40
+ const start = toValidPosition(range.start);
41
+ const end = toValidPosition(range.end);
42
+ return start && end && !isPositionBefore(end, start) ? { start, end } : null;
43
+ }
44
+
45
+ function toValidPosition(value: unknown): { line: number; character: number } | null {
46
+ if (!value || typeof value !== "object") return null;
47
+ const position = value as Record<string, unknown>;
48
+ return Number.isInteger(position.line) &&
49
+ (position.line as number) >= 0 &&
50
+ Number.isInteger(position.character) &&
51
+ (position.character as number) >= 0
52
+ ? { line: position.line as number, character: position.character as number }
53
+ : null;
54
+ }
55
+
56
+ function isPositionBefore(
57
+ left: { line: number; character: number },
58
+ right: { line: number; character: number },
59
+ ): boolean {
60
+ return left.line < right.line || (left.line === right.line && left.character < right.character);
47
61
  }
48
62
 
49
63
  const SYMBOL_KIND_NAMES: Record<number, string> = {
@@ -102,8 +116,19 @@ function flattenDocumentSymbols(
102
116
  let document: DocumentSymbol | null = null;
103
117
  if (isSymbolInformation(sym)) information = sym;
104
118
  else document = sym;
105
- const declStart = document?.range.start ?? information?.location.range.start;
106
- if (!declStart) continue;
119
+ const declarationRange = toValidRange(document?.range ?? information?.location.range);
120
+ if (!declarationRange) {
121
+ if (document?.children?.length) {
122
+ result.push(
123
+ ...flattenDocumentSymbols(document.children, filePath, sym.name, {
124
+ sourceLines: context.sourceLines,
125
+ nesting: "nested",
126
+ }),
127
+ );
128
+ }
129
+ continue;
130
+ }
131
+ const declStart = declarationRange.start;
107
132
  const nameStart = document
108
133
  ? resolveDocumentSymbolNameStart(document, context.sourceLines)
109
134
  : null;
@@ -149,8 +174,9 @@ function resolveDocumentSymbolNameStart(
149
174
  symbol: DocumentSymbol,
150
175
  sourceLines: readonly string[] | null,
151
176
  ): { line: number; character: number } | null {
152
- const selectionStart = symbol.selectionRange?.start;
153
- if (!selectionStart) return null;
177
+ const selectionRange = toValidRange(symbol.selectionRange);
178
+ if (!selectionRange) return null;
179
+ const selectionStart = selectionRange.start;
154
180
  if (!sourceLines) return selectionStart;
155
181
  const sourceLine = sourceLines[selectionStart.line];
156
182
  if (sourceLine === undefined) return null;
@@ -160,25 +186,30 @@ function resolveDocumentSymbolNameStart(
160
186
  ) {
161
187
  return selectionStart;
162
188
  }
163
- const rangeStart = symbol.range?.start;
164
- const rangeEnd = symbol.range?.end;
165
- const from = rangeStart?.line === selectionStart.line ? rangeStart.character : 0;
166
- const until = rangeEnd?.line === selectionStart.line ? rangeEnd.character : sourceLine.length;
189
+ const declarationRange = toValidRange(symbol.range);
190
+ if (!declarationRange) return null;
191
+ const from =
192
+ declarationRange.start.line === selectionStart.line ? declarationRange.start.character : 0;
193
+ const until =
194
+ declarationRange.end.line === selectionStart.line
195
+ ? declarationRange.end.character
196
+ : sourceLine.length;
167
197
  const character = sourceLine.indexOf(symbol.name, from);
168
198
  if (character < 0 || character + symbol.name.length > until) return null;
169
199
  return { line: selectionStart.line, character };
170
200
  }
171
201
 
172
- /** Map one workspace symbol to the shared representation. */
173
- export function toCodeSymbol(sym: SymbolInformation): CodeSymbol {
174
- const start = sym.location?.range?.start;
202
+ /** Map one valid workspace symbol to the shared representation. */
203
+ export function toCodeSymbol(sym: SymbolInformation): CodeSymbol | null {
204
+ const location = toCodeLocation(sym.location);
205
+ if (!location) return null;
175
206
  return {
176
207
  name: sym.name,
177
208
  kind: symbolKindName(sym.kind),
178
- file: uriToFile(sym.location?.uri ?? ""),
209
+ file: uriToFile(location.uri),
179
210
  declarationAnchor: {
180
- line: start ? start.line + 1 : 0,
181
- character: start ? start.character + 1 : 0,
211
+ line: location.range.start.line + 1,
212
+ character: location.range.start.character + 1,
182
213
  },
183
214
  container: sym.containerName ?? null,
184
215
  };
@@ -9,14 +9,19 @@
9
9
 
10
10
  // biome-ignore lint/style/noExcessiveLinesPerFile: session lifecycle, capability projection, and telemetry stay in one controller.
11
11
  import type { WorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
12
- import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
12
+ import {
13
+ recordDebugEvent,
14
+ truncateDebugIdentity as truncateIdentity,
15
+ } from "@mrclrchtr/supi-core/debug";
13
16
  import { loadConfig } from "../config/config.ts";
14
17
  import { type LspSettings, loadLspSettings } from "../config/lsp-settings.ts";
15
18
  import { clearTsconfigCache } from "../config/tsconfig-scope.ts";
16
19
  import type { DetectedProjectServer, LspConfig, ProjectServerInfo } from "../config/types.ts";
17
- import { truncateIdentity } from "../debug-telemetry.ts";
18
- import { scanWorkspaceSentinels } from "../diagnostics/workspace-sentinels.ts";
19
20
  import { LspManager, type ManagerLifecycleTransition } from "../manager/manager.ts";
21
+ import {
22
+ type AutomaticLspPathPolicy,
23
+ createAutomaticLspPathPolicy,
24
+ } from "../workspace-path-policy.ts";
20
25
  import {
21
26
  markLspCapabilitiesReady,
22
27
  registerPendingLspCapabilities,
@@ -58,6 +63,7 @@ interface LspControllerReady {
58
63
  projectServers: ProjectServerInfo[];
59
64
  detectedServers: DetectedProjectServer[];
60
65
  settings: LspSettings;
66
+ automaticPathPolicy: AutomaticLspPathPolicy;
61
67
  }
62
68
 
63
69
  interface LspControllerDisabled {
@@ -207,7 +213,6 @@ export class LspRuntimeController {
207
213
  *
208
214
  * Always attempts detected servers unless they were explicitly disabled
209
215
  * per language via `lsp.servers.<language>.enabled: false`.
210
- * The global `lsp.enabled` and `lsp.active` keys are deprecated and ignored.
211
216
  *
212
217
  * Returns the start result and updates the controller's state.
213
218
  */
@@ -220,11 +225,6 @@ export class LspRuntimeController {
220
225
  if (generation !== this.#readinessGeneration) return supersededStartResult();
221
226
 
222
227
  const lspSettings = loadLspSettings(this.#cwd);
223
- // Note: lspSettings.enabled is ignored — the global switch is deprecated.
224
- // Per-language `lsp.servers.<language>.enabled: false` is the supported
225
- // way to opt out and is already handled by loadConfig.
226
- // lspSettings.active is also ignored — the allowlist is deprecated.
227
-
228
228
  const config = loadConfig(this.#cwd);
229
229
 
230
230
  try {
@@ -287,27 +287,35 @@ export class LspRuntimeController {
287
287
  if (Object.keys(config.servers).length === 0) return this.setDisabled(generation);
288
288
  this.#state = { kind: "pending" };
289
289
 
290
+ const automaticPathPolicy = createAutomaticLspPathPolicy(this.#cwd, settings.exclude);
290
291
  let manager: LspManager;
291
- manager = new LspManager(config, this.#cwd, (transition) => {
292
- this.handleManagerLifecycle(manager, generation, transition);
293
- });
292
+ manager = new LspManager(
293
+ config,
294
+ this.#cwd,
295
+ (transition) => {
296
+ this.handleManagerLifecycle(manager, generation, transition);
297
+ },
298
+ automaticPathPolicy,
299
+ );
294
300
  this.#activeManager = manager;
295
301
  this.#latestManagerTransition = null;
296
302
  this.publishLifecycle("startup", false, []);
297
- manager.setExcludePatterns(settings.exclude);
298
303
  setWorkspaceLspRuntimeState(this.#cwd, { kind: "pending" });
299
304
 
300
- const detectedServers = scanProjectCapabilities(config, this.#cwd);
305
+ const detectedServers = scanProjectCapabilities(
306
+ config,
307
+ this.#cwd,
308
+ undefined,
309
+ automaticPathPolicy,
310
+ );
301
311
  manager.registerDetectedServers(detectedServers);
302
- await startDetectedServers(manager, detectedServers);
312
+ await startDetectedServers(manager, detectedServers, automaticPathPolicy);
303
313
  if (generation !== this.#readinessGeneration) {
304
314
  if (this.#activeManager === manager) this.#activeManager = null;
305
315
  await manager.shutdownAll();
306
316
  return supersededStartResult();
307
317
  }
308
318
 
309
- scanWorkspaceSentinels(this.#cwd);
310
-
311
319
  const runtimeOwner = createWorkspaceLspRuntimeOwner(manager);
312
320
  const workspaceRuntime = runtimeOwner.runtime;
313
321
  setWorkspaceLspRuntimeState(this.#cwd, { kind: "ready", runtime: workspaceRuntime });
@@ -333,6 +341,7 @@ export class LspRuntimeController {
333
341
  projectServers,
334
342
  detectedServers,
335
343
  settings,
344
+ automaticPathPolicy,
336
345
  };
337
346
 
338
347
  this.projectSemanticReadiness(workspaceRuntime, semanticReady);
@@ -397,7 +406,6 @@ export class LspRuntimeController {
397
406
  projectServers: projectServers.map((server) => ({
398
407
  ...server,
399
408
  fileTypes: [...server.fileTypes],
400
- supportedActions: [...server.supportedActions],
401
409
  openFiles: [...server.openFiles],
402
410
  })),
403
411
  };
@@ -452,6 +460,6 @@ export class LspRuntimeController {
452
460
  getMissingServers(): Array<{ name: string; command: string }> {
453
461
  if (this.#state.kind !== "ready") return [];
454
462
  const config = loadConfig(this.#cwd);
455
- return scanMissingServers(config, this.#cwd);
463
+ return scanMissingServers(config, this.#cwd, undefined, this.#state.automaticPathPolicy);
456
464
  }
457
465
  }
@@ -45,11 +45,37 @@ export interface OutstandingDiagnosticSummaryEntry {
45
45
  hints: number;
46
46
  }
47
47
 
48
+ /**
49
+ * Outcome of explicit process-crash recovery demand in one diagnostic pass.
50
+ *
51
+ * Routes are counted rather than client generations because process-crash
52
+ * recovery owns one budget per LSP route. `attemptedRoutes` is the number of
53
+ * crashed routes selected by the demand; a route can be failed already or
54
+ * have a shared replacement in progress when the demand observes it.
55
+ */
56
+ export interface ProcessCrashRecoverySummary {
57
+ attemptedRoutes: number;
58
+ recoveredRoutes: number;
59
+ failedRoutes: number;
60
+ }
61
+
62
+ /** Create the empty outcome for a pass with no process-crash demand. */
63
+ export function emptyProcessCrashRecoverySummary(): ProcessCrashRecoverySummary {
64
+ return {
65
+ attemptedRoutes: 0,
66
+ recoveredRoutes: 0,
67
+ failedRoutes: 0,
68
+ };
69
+ }
70
+
48
71
  /** Result from a workspace diagnostic recovery pass. */
49
72
  export interface RecoverDiagnosticsResult {
50
73
  /** Active clients targeted by the best-effort refresh, not confirmed successful refreshes. */
51
74
  attemptedClients: number;
75
+ /** Clients restarted by stale-diagnostic recovery, not process-crash recovery. */
52
76
  restartedClients: number;
77
+ /** Separate outcome for process-crash route recovery selected by this pass. */
78
+ processCrashRecovery: ProcessCrashRecoverySummary;
53
79
  /** Evidence collected by the refresh and recovery operations. */
54
80
  diagnosticEvidence: DiagnosticEvidenceSummary;
55
81
  /** Final diagnostic report captured after all refresh and recovery work. */
@@ -26,23 +26,6 @@ export function registerPendingLspCapabilities(
26
26
  runtime.registerSemanticPending(cwd, provider);
27
27
  }
28
28
 
29
- /**
30
- * Register LSP capabilities for a workspace cwd as ready.
31
- *
32
- * Wraps WorkspaceLspRuntime into a SemanticProvider via the existing semantic
33
- * adapter and publishes it into the shared workspace runtime so that
34
- * code-intelligence and other consumers can discover semantic analysis
35
- * availability.
36
- */
37
- export function registerLspCapabilities(
38
- runtime: WorkspaceRuntime,
39
- cwd: string,
40
- service: WorkspaceLspRuntime,
41
- ): void {
42
- const provider = createLspSemanticProvider(service);
43
- runtime.registerSemantic(cwd, provider);
44
- }
45
-
46
29
  /** Promote an already-registered pending semantic provider to ready. */
47
30
  export function markLspCapabilitiesReady(runtime: WorkspaceRuntime, cwd: string): void {
48
31
  runtime.markSemanticReady(cwd);
@@ -9,7 +9,11 @@ import {
9
9
  throwIfCodeRequestInterrupted,
10
10
  unavailableCodeQuery,
11
11
  } from "@mrclrchtr/supi-code-runtime/api";
12
- import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
12
+ import {
13
+ recordDebugEvent,
14
+ truncateDebugIdentity as truncateIdentity,
15
+ } from "@mrclrchtr/supi-core/debug";
16
+ import { resolveToolPath as resolveSessionPath } from "@mrclrchtr/supi-core/path";
13
17
  import { createSessionStateRegistry } from "@mrclrchtr/supi-core/session";
14
18
  import type {
15
19
  CodeAction,
@@ -26,11 +30,18 @@ import type {
26
30
  WorkspaceEdit,
27
31
  WorkspaceSymbol,
28
32
  } from "../config/types.ts";
29
- import { boundServerNames, truncateIdentity } from "../debug-telemetry.ts";
33
+ import { boundServerNames } from "../debug-telemetry.ts";
34
+ import type {
35
+ WorkspaceSentinelScanOptions,
36
+ WorkspaceSentinelSyncResult,
37
+ } from "../diagnostics/workspace-sentinels.ts";
30
38
  import type { LspManager } from "../manager/manager.ts";
31
- import { resolveSessionPath } from "../utils.ts";
32
39
  import { raceReadinessValue } from "./readiness.ts";
33
- import type { DiagnosticEvidenceSummary, RecoverDiagnosticsResult } from "./runtime-diagnostics.ts";
40
+ import type {
41
+ DiagnosticEvidenceSummary,
42
+ ProcessCrashRecoverySummary,
43
+ RecoverDiagnosticsResult,
44
+ } from "./runtime-diagnostics.ts";
34
45
  import type {
35
46
  RoutedMutationResponse,
36
47
  SemanticReadinessResult,
@@ -42,15 +53,17 @@ export type {
42
53
  ProcessCrashDiagnosticDemand,
43
54
  WorkspaceLspDiagnosticSurface,
44
55
  } from "./runtime-diagnostic-surface.ts";
45
- export type {
46
- DiagnosticEvidenceDocument,
47
- DiagnosticEvidenceStatus,
48
- DiagnosticEvidenceSummary,
49
- OutstandingDiagnosticSummaryEntry,
50
- RecoverDiagnosticsResult,
51
- WorkspaceDiagnosticReport,
52
- WorkspaceDiagnosticSnapshot,
53
- WorkspaceDiagnosticSummaryEntry,
56
+ export {
57
+ type DiagnosticEvidenceDocument,
58
+ type DiagnosticEvidenceStatus,
59
+ type DiagnosticEvidenceSummary,
60
+ emptyProcessCrashRecoverySummary,
61
+ type OutstandingDiagnosticSummaryEntry,
62
+ type ProcessCrashRecoverySummary,
63
+ type RecoverDiagnosticsResult,
64
+ type WorkspaceDiagnosticReport,
65
+ type WorkspaceDiagnosticSnapshot,
66
+ type WorkspaceDiagnosticSummaryEntry,
54
67
  } from "./runtime-diagnostics.ts";
55
68
  export type {
56
69
  RoutedMutationResponse,
@@ -67,6 +80,29 @@ function unavailableFileQuery<T>(operation: string, file: string): CodeQueryResu
67
80
  return unavailableCodeQuery(`No routed LSP client could complete ${operation} for ${file}.`);
68
81
  }
69
82
 
83
+ function hasProcessCrashRecovery(summary: ProcessCrashRecoverySummary): boolean {
84
+ return summary.attemptedRoutes > 0 || summary.recoveredRoutes > 0 || summary.failedRoutes > 0;
85
+ }
86
+
87
+ /**
88
+ * Combine the eager and current state for one readiness wait.
89
+ * A current failure replaces the older pending view; max avoids double-counting
90
+ * one route when both views describe the same recovery attempt.
91
+ */
92
+ function mergeProcessCrashRecoverySummaries(
93
+ eager: ProcessCrashRecoverySummary | undefined,
94
+ current: ProcessCrashRecoverySummary | null,
95
+ ): ProcessCrashRecoverySummary | undefined {
96
+ if (!eager) return current ?? undefined;
97
+ if (!current) return eager;
98
+ if (current.failedRoutes > 0) return current;
99
+ return {
100
+ attemptedRoutes: Math.max(eager.attemptedRoutes, current.attemptedRoutes),
101
+ recoveredRoutes: Math.max(eager.recoveredRoutes, current.recoveredRoutes),
102
+ failedRoutes: Math.max(eager.failedRoutes, current.failedRoutes),
103
+ };
104
+ }
105
+
70
106
  /** Emit one aggregate tsconfig scope-decision event after a recovery pass. */
71
107
  function recordScopeDecisionEvent(manager: LspManager): void {
72
108
  try {
@@ -246,25 +282,39 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
246
282
  ): Promise<SemanticReadinessResult> {
247
283
  const resolvedPath = this.resolveFilePath(filePath);
248
284
  if (!this.manager.canServeFile(resolvedPath)) {
285
+ const processCrashRecovery = this.manager.getProcessCrashRecoverySummaryForFile(resolvedPath);
249
286
  return {
250
287
  kind: "unavailable",
251
288
  reason: "No LSP client can serve this file",
289
+ ...(processCrashRecovery ? { processCrashRecovery } : {}),
252
290
  };
253
291
  }
254
292
 
293
+ let eagerProcessCrashRecovery: ProcessCrashRecoverySummary | undefined;
255
294
  const readiness = await raceReadinessValue(
256
- this.manager.waitUntilFileReady(resolvedPath, control),
295
+ this.manager.waitUntilFileReady(resolvedPath, control, (summary) => {
296
+ eagerProcessCrashRecovery = summary;
297
+ }),
257
298
  options.timeoutMs,
258
299
  control,
259
300
  );
260
- if (readiness.kind !== "resolved") return readiness;
261
- if (readiness.value === null) {
301
+ if (readiness.kind !== "resolved") {
302
+ const processCrashRecovery = mergeProcessCrashRecoverySummaries(
303
+ eagerProcessCrashRecovery,
304
+ this.manager.getProcessCrashRecoverySummaryForFile(resolvedPath),
305
+ );
306
+ return processCrashRecovery ? { ...readiness, processCrashRecovery } : readiness;
307
+ }
308
+ const { client, processCrashRecovery } = readiness.value;
309
+ const recovery = hasProcessCrashRecovery(processCrashRecovery) ? { processCrashRecovery } : {};
310
+ if (!client) {
262
311
  return {
263
312
  kind: "unavailable",
264
313
  reason: "The routed LSP client could not be started for this file",
314
+ ...recovery,
265
315
  };
266
316
  }
267
- return { kind: "ready" };
317
+ return { kind: "ready", ...recovery };
268
318
  }
269
319
 
270
320
  /**
@@ -294,9 +344,22 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
294
344
  return this.manager.getKnownProjectServers([]);
295
345
  }
296
346
 
297
- /** Check whether the file can be served semantically for explicit LSP operations. */
347
+ /** Check whether automatic LSP work can use and serve the source file. */
298
348
  isSupportedSourceFile(filePath: string): boolean {
299
- return this.manager.canServeFile(this.resolveFilePath(filePath));
349
+ return this.manager.isSupportedSourceFile(this.resolveFilePath(filePath));
350
+ }
351
+
352
+ /** Inventory automatic workspace sentinel and source paths. */
353
+ scanWorkspaceSentinels(options: WorkspaceSentinelScanOptions = {}): Map<string, number> {
354
+ return this.manager.scanWorkspaceSentinels(options);
355
+ }
356
+
357
+ /** Refresh the automatic workspace sentinel and source inventory. */
358
+ syncWorkspaceSentinelSnapshot(
359
+ previous: Map<string, number>,
360
+ options: WorkspaceSentinelScanOptions = {},
361
+ ): WorkspaceSentinelSyncResult {
362
+ return this.manager.syncWorkspaceSentinelSnapshot(previous, options);
300
363
  }
301
364
 
302
365
  /** Track a file in its routed client without exposing that client. */
@@ -389,6 +452,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
389
452
  elapsedMs: result.elapsedMs,
390
453
  attemptedClients: result.attemptedClients,
391
454
  restartedClients: result.restartedClients,
455
+ processCrashRecovery: result.processCrashRecovery,
392
456
  attemptedServers: boundServerNames(result.attemptedServers ?? []),
393
457
  restartedServers: boundServerNames(result.restartedServers ?? []),
394
458
  ...(result.restartReason ? { reason: result.restartReason } : {}),
@@ -1,6 +1,9 @@
1
- import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
1
+ import {
2
+ recordDebugEvent,
3
+ truncateDebugIdentity as truncateIdentity,
4
+ } from "@mrclrchtr/supi-core/debug";
2
5
  import type { ProjectServerInfo } from "../config/types.ts";
3
- import { MAX_SERVERS, truncateIdentity } from "../debug-telemetry.ts";
6
+ import { MAX_SERVERS } from "../debug-telemetry.ts";
4
7
  import type { LspRuntimeTransition } from "./runtime-controller.ts";
5
8
 
6
9
  /** One bounded server entry in a runtime-transition payload. */
@@ -1,15 +1,19 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { dedupeTopmostRoots, walkProject } from "@mrclrchtr/supi-core/project";
3
+ import { dedupeTopmostRoots } from "@mrclrchtr/supi-core/project";
4
4
  import type {
5
5
  DetectedProjectServer,
6
6
  LspConfig,
7
7
  MissingServer,
8
- ProjectServerInfo,
9
8
  ServerConfig,
10
9
  } from "../config/types.ts";
11
10
  import type { LspManager } from "../manager/manager.ts";
12
11
  import { commandExists } from "../utils.ts";
12
+ import {
13
+ type AutomaticLspPathPolicy,
14
+ createDefaultAutomaticLspPathPolicy,
15
+ walkAutomaticLspTree,
16
+ } from "../workspace-path-policy.ts";
13
17
 
14
18
  const DEFAULT_MAX_DEPTH = 3;
15
19
 
@@ -17,8 +21,14 @@ export function scanProjectCapabilities(
17
21
  config: LspConfig,
18
22
  cwd: string,
19
23
  maxDepth: number = DEFAULT_MAX_DEPTH,
24
+ policy: AutomaticLspPathPolicy = createDefaultAutomaticLspPathPolicy(cwd),
20
25
  ): DetectedProjectServer[] {
21
- const { markerMatches, extensionBasedServers } = collectServerHints(config, cwd, maxDepth);
26
+ const { markerMatches, extensionBasedServers } = collectServerHints(
27
+ config,
28
+ cwd,
29
+ maxDepth,
30
+ policy,
31
+ );
22
32
 
23
33
  return Object.entries(config.servers)
24
34
  .flatMap(([serverName, server]) => {
@@ -45,11 +55,12 @@ function collectServerHints(
45
55
  config: LspConfig,
46
56
  cwd: string,
47
57
  maxDepth: number,
58
+ policy: AutomaticLspPathPolicy,
48
59
  ): { markerMatches: Map<string, Set<string>>; extensionBasedServers: Set<string> } {
49
60
  const ctx: HintContext = { markerMatches: new Map(), extensionBasedServers: new Set() };
50
61
 
51
- walkProject(cwd, maxDepth, (directory, entryNames) => {
52
- inspectDirectory(directory, entryNames, config.servers, ctx);
62
+ walkAutomaticLspTree(policy, cwd, maxDepth, (directory, entries) => {
63
+ inspectDirectory(directory, new Set(entries.map((entry) => entry.name)), config.servers, ctx);
53
64
  });
54
65
 
55
66
  return { markerMatches: ctx.markerMatches, extensionBasedServers: ctx.extensionBasedServers };
@@ -103,15 +114,10 @@ function hasMatchingFile(directory: string, entryNames: Set<string>, fileTypes:
103
114
  export async function startDetectedServers(
104
115
  manager: LspManager,
105
116
  detected: DetectedProjectServer[],
117
+ policy: AutomaticLspPathPolicy = createDefaultAutomaticLspPathPolicy(manager.getCwd()),
106
118
  ): Promise<void> {
107
- await Promise.all(detected.map((entry) => manager.startServerForRoot(entry.name, entry.root)));
108
- }
109
-
110
- export function introspectCapabilities(
111
- manager: LspManager,
112
- detected: DetectedProjectServer[],
113
- ): ProjectServerInfo[] {
114
- return manager.getKnownProjectServers(detected);
119
+ const eligible = detected.filter((entry) => policy.isEligible(entry.root, "directory"));
120
+ await Promise.all(eligible.map((entry) => manager.startServerForRoot(entry.name, entry.root)));
115
121
  }
116
122
 
117
123
  /**
@@ -123,17 +129,14 @@ export function scanMissingServers(
123
129
  config: LspConfig,
124
130
  cwd: string,
125
131
  maxDepth: number = DEFAULT_MAX_DEPTH,
132
+ policy: AutomaticLspPathPolicy = createDefaultAutomaticLspPathPolicy(cwd),
126
133
  ): MissingServer[] {
127
134
  const foundExtensions = new Set<string>();
128
135
 
129
- walkProject(cwd, maxDepth, (directory, entryNames) => {
130
- for (const name of entryNames) {
131
- try {
132
- if (!fs.statSync(path.join(directory, name)).isFile()) continue;
133
- } catch {
134
- continue;
135
- }
136
- const ext = path.extname(name).slice(1).toLowerCase();
136
+ walkAutomaticLspTree(policy, cwd, maxDepth, (_directory, entries) => {
137
+ for (const entry of entries) {
138
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
139
+ const ext = path.extname(entry.name).slice(1).toLowerCase();
137
140
  if (ext) foundExtensions.add(ext);
138
141
  }
139
142
  });
@@ -13,7 +13,12 @@ import type {
13
13
  WorkspaceEdit,
14
14
  WorkspaceSymbol,
15
15
  } from "../config/types.ts";
16
+ import type {
17
+ WorkspaceSentinelScanOptions,
18
+ WorkspaceSentinelSyncResult,
19
+ } from "../diagnostics/workspace-sentinels.ts";
16
20
  import type { WorkspaceLspDiagnosticSurface } from "./runtime-diagnostic-surface.ts";
21
+ import type { ProcessCrashRecoverySummary } from "./runtime-diagnostics.ts";
17
22
 
18
23
  export type WorkspaceLspRuntimeState =
19
24
  | { kind: "ready"; runtime: WorkspaceLspRuntime }
@@ -23,9 +28,22 @@ export type WorkspaceLspRuntimeState =
23
28
  | { kind: "unavailable"; reason: string };
24
29
 
25
30
  export type SemanticReadinessResult =
26
- | { kind: "ready" }
27
- | { kind: "timeout" }
28
- | { kind: "unavailable"; reason: string };
31
+ | {
32
+ kind: "ready";
33
+ /** Process-crash route recovery observed while establishing file readiness. */
34
+ processCrashRecovery?: ProcessCrashRecoverySummary;
35
+ }
36
+ | {
37
+ kind: "timeout";
38
+ /** Process-crash route recovery observed before the readiness timeout. */
39
+ processCrashRecovery?: ProcessCrashRecoverySummary;
40
+ }
41
+ | {
42
+ kind: "unavailable";
43
+ reason: string;
44
+ /** Process-crash route recovery observed before readiness became unavailable. */
45
+ processCrashRecovery?: ProcessCrashRecoverySummary;
46
+ };
29
47
 
30
48
  /** One mutation response and the exact provider roots from its semantic route. */
31
49
  export interface RoutedMutationResponse<T> {
@@ -96,7 +114,15 @@ export interface WorkspaceLspRuntime extends WorkspaceLspDiagnosticSurface {
96
114
  control?: CodeRequestControl,
97
115
  ): Promise<SemanticReadinessResult>;
98
116
  getProjectServers(): ProjectServerInfo[];
117
+ /** Check whether automatic LSP work can use and serve the source file. */
99
118
  isSupportedSourceFile(filePath: string): boolean;
119
+ /** Inventory policy-eligible workspace sentinels and optional source files. */
120
+ scanWorkspaceSentinels(options?: WorkspaceSentinelScanOptions): Map<string, number>;
121
+ /** Refresh one policy-eligible workspace sentinel and source inventory. */
122
+ syncWorkspaceSentinelSnapshot(
123
+ previous: Map<string, number>,
124
+ options?: WorkspaceSentinelScanOptions,
125
+ ): WorkspaceSentinelSyncResult;
100
126
  trackFile(filePath: string): Promise<boolean>;
101
127
  closeFile(filePath: string): void;
102
128
  pruneMissingFiles(): readonly string[];