@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
@@ -21,9 +21,18 @@ import type { ClientDiagnosticSnapshot } from "../client/client-document-state.t
21
21
 
22
22
  export { RECOVERY_CLIENT_STARTUP_BOUND_MS } from "../client/client.ts";
23
23
 
24
- import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
24
+ import {
25
+ recordDebugEvent,
26
+ truncateDebugIdentity as truncateIdentity,
27
+ } from "@mrclrchtr/supi-core/debug";
28
+ import { resolveToolPath as resolveSessionPath, uriToFile } from "@mrclrchtr/supi-core/path";
25
29
  import { getServerForFile } from "../config/config.ts";
26
- import { getFileScopeDecision } from "../config/tsconfig-scope.ts";
30
+ import {
31
+ getFileScopeDecision,
32
+ invalidateTsconfigCacheForConfig,
33
+ invalidateTsconfigCacheForConfigDir,
34
+ isProjectConfigFileName,
35
+ } from "../config/tsconfig-scope.ts";
27
36
  import type {
28
37
  DetectedProjectServer,
29
38
  Diagnostic,
@@ -35,7 +44,8 @@ import type {
35
44
  SymbolInformation,
36
45
  WorkspaceSymbol,
37
46
  } from "../config/types.ts";
38
- import { boundCwd, truncateIdentity } from "../debug-telemetry.ts";
47
+ import { FileChangeType } from "../config/types.ts";
48
+ import { boundCwd } from "../debug-telemetry.ts";
39
49
  import {
40
50
  accumulateOutstandingDiagnostics,
41
51
  collectDiagnosticSummaryCounts,
@@ -47,11 +57,19 @@ import {
47
57
  type DiagnosticEvidenceSummary,
48
58
  summarizeDiagnosticEvidence,
49
59
  } from "../diagnostics/evidence.ts";
60
+ import {
61
+ scanWorkspaceSentinels as scanAutomaticWorkspaceSentinels,
62
+ syncWorkspaceSentinelSnapshot as syncAutomaticWorkspaceSentinelSnapshot,
63
+ type WorkspaceSentinelScanOptions,
64
+ type WorkspaceSentinelSyncResult,
65
+ } from "../diagnostics/workspace-sentinels.ts";
50
66
  import { raceRequestControl } from "../session/readiness.ts";
51
- import type {
52
- ScopeDecisionEntry,
53
- ScopeDecisionSummary,
54
- WorkspaceDiagnosticReport,
67
+ import {
68
+ emptyProcessCrashRecoverySummary,
69
+ type ProcessCrashRecoverySummary,
70
+ type ScopeDecisionEntry,
71
+ type ScopeDecisionSummary,
72
+ type WorkspaceDiagnosticReport,
55
73
  } from "../session/runtime-diagnostics.ts";
56
74
  import {
57
75
  displayRelativeFilePath,
@@ -61,7 +79,11 @@ import {
61
79
  normalizeRelevantPaths,
62
80
  shouldIgnoreLspPath,
63
81
  } from "../summary.ts";
64
- import { commandExists, resolveSessionPath, uriToFile } from "../utils.ts";
82
+ import { commandExists } from "../utils.ts";
83
+ import {
84
+ type AutomaticLspPathPolicy,
85
+ createDefaultAutomaticLspPathPolicy,
86
+ } from "../workspace-path-policy.ts";
65
87
  import {
66
88
  closeFileAcrossClients,
67
89
  pruneMissingFilesFromClients,
@@ -71,12 +93,7 @@ import {
71
93
  collectOutstandingDiagnosticsDetailed,
72
94
  syncClientFileAndGetDiagnostics,
73
95
  } from "./manager-diagnostics.ts";
74
- import {
75
- clientKey,
76
- isExcludedByPattern,
77
- rememberKnownRoot,
78
- resolveRootForFile,
79
- } from "./manager-helpers.ts";
96
+ import { clientKey, rememberKnownRoot, resolveRootForFile } from "./manager-helpers.ts";
80
97
  import { buildProjectServerInfo } from "./manager-project-info.ts";
81
98
  import type {
82
99
  ActiveCoverageSummaryEntry,
@@ -161,6 +178,11 @@ type FileClientRequestOptions = {
161
178
  control?: CodeRequestControl;
162
179
  };
163
180
 
181
+ interface FileReadinessResult {
182
+ client: LspClient | null;
183
+ processCrashRecovery: ProcessCrashRecoverySummary;
184
+ }
185
+
164
186
  interface ProcessCrashRecoveryState {
165
187
  /** The one automatic attempt is consumed when a replacement starts. */
166
188
  attemptConsumed: boolean;
@@ -181,6 +203,8 @@ type ProcessCrashRecoveryOutcome = "attempt" | "success" | "failure" | "exhauste
181
203
  type ProcessCrashDemandResult = {
182
204
  /** Whether at least one required crashed route supports the operation. */
183
205
  hasSupport: boolean;
206
+ /** Separate route-level result for this explicit process-crash demand. */
207
+ processCrashRecovery: ProcessCrashRecoverySummary;
184
208
  /** Required routes that remain unavailable after shared recovery settles. */
185
209
  failures: string[];
186
210
  /** In-scope tracked files from required routes that remain unavailable. */
@@ -193,6 +217,20 @@ type RequiredProcessCrashDemand = {
193
217
  failedFiles: readonly string[];
194
218
  };
195
219
 
220
+ type ProcessCrashRecoveryRouteOutcome = "pending" | "recovered" | "failed";
221
+
222
+ function summarizeProcessCrashRecoveryRoutes(
223
+ outcomes: Iterable<ProcessCrashRecoveryRouteOutcome>,
224
+ ): ProcessCrashRecoverySummary {
225
+ const summary = emptyProcessCrashRecoverySummary();
226
+ for (const outcome of outcomes) {
227
+ summary.attemptedRoutes++;
228
+ if (outcome === "recovered") summary.recoveredRoutes++;
229
+ if (outcome === "failed") summary.failedRoutes++;
230
+ }
231
+ return summary;
232
+ }
233
+
196
234
  // ── LspManager ────────────────────────────────────────────────────────
197
235
  export class LspManager {
198
236
  /** Active clients keyed by "serverName:root" */
@@ -201,14 +239,14 @@ export class LspManager {
201
239
  private unavailable = new Map<string, UnavailableReason>();
202
240
  /** Memoized per-command availability of LSP server binaries on PATH */
203
241
  private commandAvailability = new Map<string, boolean>();
204
- /** Guards against concurrent client creation for the same server:root key */
242
+ /** Guards against concurrent client creation for the same server:root key. */
205
243
  private pendingStarts = new Map<string, Promise<LspClient | null>>();
244
+ /** Shared diagnostic-recovery restart for each replaceable client route. */
245
+ private pendingRestarts = new Map<string, Promise<{ files: string[]; restarted: boolean }>>();
206
246
  /** Monotonic ownership generation for each replaceable client route. */
207
247
  private clientGenerations = new Map<string, number>();
208
248
  /** Preferred project roots discovered by proactive scan or lazy startup */
209
249
  private knownRoots = new Map<string, string[]>();
210
- /** User-configured gitignore-style exclude patterns */
211
- private excludePatterns: string[] = [];
212
250
  /** Monotonic workspace invalidation generation advanced by each invalidation event. */
213
251
  private invalidationEpoch = 0;
214
252
  /** Invalidation generation in which each route last received a recovery restart. */
@@ -231,17 +269,19 @@ export class LspManager {
231
269
  private readonly config: LspConfig,
232
270
  private readonly cwd: string,
233
271
  private readonly onLifecycleTransition?: ManagerLifecycleListener,
272
+ private readonly automaticPathPolicy: AutomaticLspPathPolicy = createDefaultAutomaticLspPathPolicy(
273
+ cwd,
274
+ ),
234
275
  ) {}
235
276
  getCwd(): string {
236
277
  return this.cwd;
237
278
  }
238
- setExcludePatterns(patterns: string[]): void {
239
- this.excludePatterns = patterns;
240
- }
241
279
 
242
280
  // ── Public API ────────────────────────────────────────────────────
243
281
  registerDetectedServers(detected: DetectedProjectServer[]): void {
244
- this.knownRoots = projectRoots.buildKnownRootsMap(detected);
282
+ this.knownRoots = projectRoots.buildKnownRootsMap(
283
+ detected.filter((entry) => this.automaticPathPolicy.isEligible(entry.root, "directory")),
284
+ );
245
285
  }
246
286
  /** Check whether a file path has an available LSP server for explicit semantic operations. */
247
287
  canServeFile(filePath: string): boolean {
@@ -252,18 +292,56 @@ export class LspManager {
252
292
  }
253
293
 
254
294
  /**
255
- * Check whether a file should participate in runtime guidance and diagnostics.
256
- * This is stricter than {@link canServeFile} and intentionally filters dependency
257
- * and tsconfig-excluded paths from UI/context behavior.
295
+ * Return the consumed process-crash outcome for a file readiness result.
296
+ * The result supports unavailable and non-resolved waits; it is null before
297
+ * an attempt is consumed and after successful recovery, so old success is
298
+ * not reported as a new outcome. This is a passive read and never starts
299
+ * recovery.
300
+ */
301
+ getProcessCrashRecoverySummaryForFile(filePath: string): ProcessCrashRecoverySummary | null {
302
+ const route = this.resolveFileRoute(resolveSessionPath(this.cwd, filePath));
303
+ const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
304
+ if (!recovery?.attemptConsumed || recovery.statusReason === undefined) return null;
305
+
306
+ const outcome: ProcessCrashRecoveryRouteOutcome =
307
+ recovery.statusReason === "process-crash-recovery-exhausted" ? "failed" : "pending";
308
+ return summarizeProcessCrashRecoveryRoutes([outcome]);
309
+ }
310
+
311
+ /**
312
+ * Check whether a file should participate in automatic LSP work.
313
+ * This is stricter than {@link canServeFile} and applies the workspace path
314
+ * policy plus tsconfig diagnostic suppression.
258
315
  */
259
316
  isSupportedSourceFile(filePath: string): boolean {
260
- // Dependency directories are intentionally excluded from recent-path
261
- // tracking and diagnostic summaries (shouldIgnoreLspPath). Keep runtime
262
- // guidance activation consistent: reading or editing a file under
263
- // node_modules / .pnpm must not arm LSP guidance for dependency sources.
264
- if (shouldIgnoreLspPath(filePath, this.cwd)) return false;
317
+ if (!this.isAutomaticScopePath(filePath)) return false;
265
318
  return this.canServeFile(filePath);
266
319
  }
320
+
321
+ /** Inventory automatic workspace sentinel and source paths with the runtime policy. */
322
+ scanWorkspaceSentinels(options: WorkspaceSentinelScanOptions = {}): Map<string, number> {
323
+ return scanAutomaticWorkspaceSentinels(this.cwd, {
324
+ ...options,
325
+ policy: this.automaticPathPolicy,
326
+ });
327
+ }
328
+
329
+ /** Refresh the automatic workspace inventory with the runtime policy. */
330
+ syncWorkspaceSentinelSnapshot(
331
+ previous: Map<string, number>,
332
+ options: WorkspaceSentinelScanOptions = {},
333
+ ): WorkspaceSentinelSyncResult {
334
+ return syncAutomaticWorkspaceSentinelSnapshot(this.cwd, previous, {
335
+ ...options,
336
+ policy: this.automaticPathPolicy,
337
+ });
338
+ }
339
+
340
+ /** Apply automatic path exclusions plus diagnostic tsconfig suppression. */
341
+ private isAutomaticScopePath(filePath: string): boolean {
342
+ return !shouldIgnoreLspPath(filePath, this.cwd, this.automaticPathPolicy);
343
+ }
344
+
267
345
  private isServerCommandAvailable(command: string): boolean {
268
346
  // Only memoize positive lookups. A negative result may become stale if the
269
347
  // user installs the binary mid-session (e.g. `mise install`), and
@@ -481,7 +559,14 @@ export class LspManager {
481
559
  return failedFiles === null ? [] : [{ key, recovery, failedFiles }];
482
560
  },
483
561
  );
484
- if (required.length === 0) return { hasSupport: false, failures: [], failedFiles: [] };
562
+ if (required.length === 0) {
563
+ return {
564
+ hasSupport: false,
565
+ processCrashRecovery: emptyProcessCrashRecoverySummary(),
566
+ failures: [],
567
+ failedFiles: [],
568
+ };
569
+ }
485
570
 
486
571
  await Promise.all(
487
572
  required.map(async ({ key, recovery }) => {
@@ -501,8 +586,13 @@ export class LspManager {
501
586
  );
502
587
 
503
588
  const failedDemands = required.filter(({ recovery }) => recovery.statusReason !== undefined);
589
+ const outcomes = required.map(
590
+ ({ recovery }): ProcessCrashRecoveryRouteOutcome =>
591
+ recovery.statusReason === undefined ? "recovered" : "failed",
592
+ );
504
593
  return {
505
594
  hasSupport: true,
595
+ processCrashRecovery: summarizeProcessCrashRecoveryRoutes(outcomes),
506
596
  failures: failedDemands.map(({ recovery }) => this.formatProcessCrashDemandFailure(recovery)),
507
597
  failedFiles: Array.from(new Set(failedDemands.flatMap(({ failedFiles }) => failedFiles))),
508
598
  };
@@ -808,32 +898,60 @@ export class LspManager {
808
898
  const seen = new Set<string>();
809
899
 
810
900
  for (const filePath of filePaths) {
811
- const client = this.getExistingClientForFile(filePath);
812
- if (!client) continue;
813
- if (options?.pushOnly && client.hasDiagnosticProvider) continue;
814
-
815
- const key = clientKey(client.name, client.root);
816
- const processRecovery = this.processCrashRecoveries.get(key);
817
- if (processRecovery?.pending) continue;
818
- if (seen.has(key)) continue;
819
- seen.add(key);
820
- if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) continue;
901
+ const candidate = this.getRestartCandidate(filePath, options?.pushOnly === true, seen);
902
+ if (!candidate) continue;
903
+ const { client, key } = candidate;
821
904
 
822
905
  // Observe cancellation between route restarts so a cancelled pass does
823
906
  // not keep restarting further clients.
824
907
  throwIfCodeRequestInterrupted(options?.control);
825
- const result = await this.restartClient(client);
826
- this.recoveryRestartEpochs.set(key, this.invalidationEpoch);
908
+ const restartPromise = this.pendingRestarts.get(key) ?? this.startSharedRestart(client, key);
909
+ const result = await raceRequestControl(restartPromise, options?.control);
827
910
  restarted.push({ key, serverName: client.name, ...result });
828
911
  }
829
912
 
830
913
  return restarted;
831
914
  }
832
915
 
916
+ private startSharedRestart(
917
+ client: LspClient,
918
+ key: string,
919
+ ): Promise<{ files: string[]; restarted: boolean }> {
920
+ const restartEpoch = this.invalidationEpoch;
921
+ const restartPromise = this.restartClient(client).then((result) => {
922
+ this.recoveryRestartEpochs.set(key, restartEpoch);
923
+ return result;
924
+ });
925
+ this.pendingRestarts.set(key, restartPromise);
926
+ void restartPromise
927
+ .finally(() => {
928
+ if (this.pendingRestarts.get(key) === restartPromise) this.pendingRestarts.delete(key);
929
+ })
930
+ .catch(() => {});
931
+ return restartPromise;
932
+ }
933
+
934
+ private getRestartCandidate(
935
+ filePath: string,
936
+ pushOnly: boolean,
937
+ seen: Set<string>,
938
+ ): { client: LspClient; key: string } | null {
939
+ const client = this.getExistingClientForFile(filePath);
940
+ if (!client || (pushOnly && client.hasDiagnosticProvider)) return null;
941
+
942
+ const key = clientKey(client.name, client.root);
943
+ if (this.processCrashRecoveries.get(key)?.pending || seen.has(key)) return null;
944
+ seen.add(key);
945
+ if (this.recoveryRestartEpochs.get(key) === this.invalidationEpoch) return null;
946
+ return { client, key };
947
+ }
948
+
833
949
  private async restartClient(client: LspClient): Promise<{ files: string[]; restarted: boolean }> {
834
950
  const key = clientKey(client.name, client.root);
835
951
  const serverConfig = this.config.servers[client.name];
836
- if (!serverConfig) return { files: [], restarted: false };
952
+ if (!serverConfig || this.clients.get(key) !== client) {
953
+ return { files: [], restarted: false };
954
+ }
837
955
 
838
956
  const openFiles = client.openFiles;
839
957
  const trackedDiagnosticFiles = client
@@ -846,6 +964,7 @@ export class LspManager {
846
964
  // Ignore shutdown failures when forcing a restart.
847
965
  }
848
966
 
967
+ if (this.clients.get(key) !== client) return { files, restarted: false };
849
968
  this.clients.delete(key);
850
969
  this.unavailable.delete(key);
851
970
  this.clearWarmedWorkspaceSymbolProjects(client.name, client.root);
@@ -862,12 +981,18 @@ export class LspManager {
862
981
  RECOVERY_CLIENT_STARTUP_BOUND_MS,
863
982
  "replacement client startup bound exceeded",
864
983
  );
984
+ if (this.clients.get(key) !== replacement || this.shuttingDown) {
985
+ void replacement.forceKill().catch(() => {});
986
+ return { files, restarted: false };
987
+ }
865
988
  this.restoreTrackedDocuments(replacement, files);
866
989
  this.clearUnconsumedProcessCrashState(key, client);
867
990
  return { files, restarted: true };
868
991
  } catch {
869
- this.clients.delete(key);
870
- this.unavailable.set(key, "start-failed");
992
+ if (this.clients.get(key) === replacement) {
993
+ this.clients.delete(key);
994
+ this.unavailable.set(key, "start-failed");
995
+ }
871
996
  // A bound-exceeded start may have spawned a live process; terminate
872
997
  // the tree so the orphan cannot outlive the failed restart.
873
998
  void replacement.forceKill().catch(() => {});
@@ -893,6 +1018,7 @@ export class LspManager {
893
1018
  unavailableReason:
894
1019
  this.getUnavailableReason(key, this.config.servers[serverName]?.command) ?? undefined,
895
1020
  statusReason: this.processCrashRecoveries.get(key)?.statusReason,
1021
+ includeOpenFile: (file) => this.isAutomaticScopePath(file),
896
1022
  },
897
1023
  this.cwd,
898
1024
  );
@@ -900,6 +1026,7 @@ export class LspManager {
900
1026
  getKnownProjectServers(detected: DetectedProjectServer[]): ProjectServerInfo[] {
901
1027
  const known = new Map<string, DetectedProjectServer>();
902
1028
  for (const entry of detected) {
1029
+ if (!this.automaticPathPolicy.isEligible(entry.root, "directory")) continue;
903
1030
  known.set(clientKey(entry.name, entry.root), entry);
904
1031
  }
905
1032
  for (const client of this.clients.values()) {
@@ -938,20 +1065,31 @@ export class LspManager {
938
1065
  async waitUntilFileReady(
939
1066
  filePath: string,
940
1067
  control?: CodeRequestControl,
941
- ): Promise<LspClient | null> {
1068
+ onProcessCrashRecovery?: (summary: ProcessCrashRecoverySummary) => void,
1069
+ ): Promise<FileReadinessResult> {
942
1070
  throwIfCodeRequestInterrupted(control);
943
1071
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
1072
+ const route = this.resolveFileRoute(resolvedPath);
1073
+ const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
1074
+ const processCrashRecoveryRequested =
1075
+ recovery !== undefined && (recovery.pending !== null || recovery.statusReason !== undefined);
1076
+ if (processCrashRecoveryRequested) {
1077
+ onProcessCrashRecovery?.(summarizeProcessCrashRecoveryRoutes(["pending"]));
1078
+ }
944
1079
  const client = await this.getClientForFile(resolvedPath, {
945
1080
  recoverProcessCrash: true,
946
1081
  control,
947
1082
  });
948
- if (!client) return null;
1083
+ const processCrashRecovery = processCrashRecoveryRequested
1084
+ ? summarizeProcessCrashRecoveryRoutes([client ? "recovered" : "failed"])
1085
+ : emptyProcessCrashRecoverySummary();
1086
+ if (!client) return { client: null, processCrashRecovery };
949
1087
  // The caller's wait stops on cancellation even though the shared
950
1088
  // readiness state keeps its own lifecycle.
951
1089
  await client.getReady(control);
952
1090
  await this.warmSemanticProject(client, resolvedPath, true, control);
953
1091
  throwIfCodeRequestInterrupted(control);
954
- return client;
1092
+ return { client, processCrashRecovery };
955
1093
  }
956
1094
 
957
1095
  /**
@@ -989,6 +1127,7 @@ export class LspManager {
989
1127
  firstReady.root,
990
1128
  serverConfig.rootMarkers,
991
1129
  serverConfig.fileTypes,
1130
+ { policy: this.automaticPathPolicy },
992
1131
  )[0];
993
1132
  if (target) await this.warmSemanticProject(firstReady, target.file, false, control);
994
1133
  }
@@ -1060,6 +1199,7 @@ export class LspManager {
1060
1199
  */
1061
1200
  noteWorkspaceChanges(changes: FileEvent[]): void {
1062
1201
  if (changes.length > 0) this.invalidationEpoch++;
1202
+ invalidateProjectConfigCaches(changes);
1063
1203
  this.notifyWorkspaceFileChanges(changes);
1064
1204
  }
1065
1205
 
@@ -1110,12 +1250,20 @@ export class LspManager {
1110
1250
  const files = this.processCrashDiagnosticFiles(recovery, options.processCrashDemand);
1111
1251
  return files.length > 0 ? files : null;
1112
1252
  }, options.control)
1113
- : { hasSupport: false, failures: [], failedFiles: [] };
1253
+ : {
1254
+ hasSupport: false,
1255
+ processCrashRecovery: emptyProcessCrashRecoverySummary(),
1256
+ failures: [],
1257
+ failedFiles: [],
1258
+ };
1114
1259
  const recoveryOptions = demand.hasSupport
1115
1260
  ? { ...options, initialEvidence: undefined }
1116
1261
  : options;
1117
1262
  const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
1118
- return this.addProcessCrashDiagnosticEvidence(result, demand);
1263
+ return this.addProcessCrashDiagnosticEvidence(
1264
+ { ...result, processCrashRecovery: demand.processCrashRecovery },
1265
+ demand,
1266
+ );
1119
1267
  }
1120
1268
 
1121
1269
  private processCrashDiagnosticFiles(
@@ -1184,6 +1332,7 @@ export class LspManager {
1184
1332
  this.warmedWorkspaceSymbolProjects.clear();
1185
1333
  this.warmedSemanticProjects.clear();
1186
1334
  this.pendingWarmProbes.clear();
1335
+ this.pendingRestarts.clear();
1187
1336
  }
1188
1337
  /** Get status of all servers. */
1189
1338
  getRunningClientCount(): number {
@@ -1197,13 +1346,9 @@ export class LspManager {
1197
1346
  .map((client) => client.name);
1198
1347
  }
1199
1348
 
1200
- /** Check whether a path belongs to the configured diagnostic evidence scope. */
1349
+ /** Check whether a path belongs to the automatic diagnostic evidence scope. */
1201
1350
  isDiagnosticFile(filePath: string): boolean {
1202
- const relative = path.relative(this.cwd, path.resolve(this.cwd, filePath));
1203
- return (
1204
- !shouldIgnoreLspPath(relative, this.cwd) &&
1205
- !isExcludedByPattern(relative, this.excludePatterns)
1206
- );
1351
+ return this.isAutomaticScopePath(filePath);
1207
1352
  }
1208
1353
 
1209
1354
  getDiagnosticEvidence(): DiagnosticEvidenceSummary {
@@ -1218,7 +1363,7 @@ export class LspManager {
1218
1363
  name: client.name,
1219
1364
  status: client.status === "running" ? "running" : "error",
1220
1365
  root: client.root,
1221
- openFiles: client.openFiles,
1366
+ openFiles: client.openFiles.filter((file) => this.isAutomaticScopePath(file)),
1222
1367
  });
1223
1368
  }
1224
1369
  return { servers };
@@ -1259,8 +1404,7 @@ export class LspManager {
1259
1404
  const openFiles = activeServers.get(server.name) ?? new Set<string>();
1260
1405
  for (const file of server.openFiles) {
1261
1406
  const relativeFile = displayRelativeFilePath(file, this.cwd);
1262
- if (shouldIgnoreLspPath(relativeFile, this.cwd)) continue;
1263
- if (isExcludedByPattern(relativeFile, this.excludePatterns)) continue;
1407
+ if (!this.isAutomaticScopePath(relativeFile)) continue;
1264
1408
  openFiles.add(relativeFile);
1265
1409
  }
1266
1410
  activeServers.set(server.name, openFiles);
@@ -1286,7 +1430,7 @@ export class LspManager {
1286
1430
  .map((entry) => ({
1287
1431
  ...entry,
1288
1432
  openFiles: entry.openFiles.filter((file) =>
1289
- isPathRelevant(file, normalizedPaths, this.cwd),
1433
+ isPathRelevant(file, normalizedPaths, this.cwd, this.automaticPathPolicy),
1290
1434
  ),
1291
1435
  }))
1292
1436
  .filter((entry) => entry.openFiles.length > 0);
@@ -1318,7 +1462,9 @@ export class LspManager {
1318
1462
  const fileDiags = new Map<string, { errors: number; warnings: number }>();
1319
1463
  for (const snapshot of snapshots) {
1320
1464
  for (const entry of snapshot.entries) {
1321
- collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
1465
+ collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, (file) =>
1466
+ this.isAutomaticScopePath(file),
1467
+ );
1322
1468
  }
1323
1469
  }
1324
1470
 
@@ -1332,7 +1478,7 @@ export class LspManager {
1332
1478
  entries: collectOutstandingDiagnosticsDetailed(
1333
1479
  snapshots.map((snapshot) => snapshot.entries),
1334
1480
  this.cwd,
1335
- this.excludePatterns,
1481
+ (file) => this.isAutomaticScopePath(file),
1336
1482
  maxSeverity,
1337
1483
  ),
1338
1484
  current,
@@ -1364,7 +1510,9 @@ export class LspManager {
1364
1510
  });
1365
1511
  }
1366
1512
  for (const entry of snapshot.entries) {
1367
- collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
1513
+ collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, (file) =>
1514
+ this.isAutomaticScopePath(file),
1515
+ );
1368
1516
  }
1369
1517
  }
1370
1518
  const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
@@ -1396,7 +1544,7 @@ export class LspManager {
1396
1544
  entries: collectOutstandingDiagnosticSummaryEntries(
1397
1545
  snapshots,
1398
1546
  this.cwd,
1399
- this.excludePatterns,
1547
+ (file) => this.isAutomaticScopePath(file),
1400
1548
  maxSeverity,
1401
1549
  ),
1402
1550
  current:
@@ -1413,7 +1561,7 @@ export class LspManager {
1413
1561
  if (normalizedPaths.length === 0) return null;
1414
1562
  return formatOutstandingDiagnosticsSummaryText(
1415
1563
  this.getOutstandingDiagnosticSummary(maxSeverity).filter((entry) =>
1416
- isPathRelevant(entry.file, normalizedPaths, this.cwd),
1564
+ isPathRelevant(entry.file, normalizedPaths, this.cwd, this.automaticPathPolicy),
1417
1565
  ),
1418
1566
  maxFiles,
1419
1567
  );
@@ -1443,7 +1591,7 @@ export class LspManager {
1443
1591
  entries: collectOutstandingDiagnosticsDetailed(
1444
1592
  snapshots.map((snapshot) => snapshot.entries),
1445
1593
  this.cwd,
1446
- this.excludePatterns,
1594
+ (file) => this.isAutomaticScopePath(file),
1447
1595
  maxSeverity,
1448
1596
  ),
1449
1597
  current:
@@ -1472,6 +1620,7 @@ export class LspManager {
1472
1620
  for (const client of this.clients.values()) {
1473
1621
  for (const document of client.getDiagnosticSnapshot().documents) {
1474
1622
  const file = relativeFilePathFromUri(document.uri, this.cwd);
1623
+ if (!this.isAutomaticScopePath(file)) continue;
1475
1624
  totalFiles++;
1476
1625
  accumulateScopeDecision(accumulator, file, getFileScopeDecision(file, this.cwd));
1477
1626
  }
@@ -1491,8 +1640,7 @@ export class LspManager {
1491
1640
  const byFile = new Map<string, DiagnosticEvidenceDocument>();
1492
1641
  for (const document of documents) {
1493
1642
  const file = path.relative(this.cwd, path.resolve(this.cwd, document.file));
1494
- if (shouldIgnoreLspPath(file, this.cwd)) continue;
1495
- if (isExcludedByPattern(file, this.excludePatterns)) continue;
1643
+ if (!this.isAutomaticScopePath(file)) continue;
1496
1644
  const existing = byFile.get(file);
1497
1645
  if (!existing || evidenceStatusRank(document.status) > evidenceStatusRank(existing.status)) {
1498
1646
  byFile.set(file, { file, status: document.status });
@@ -1539,7 +1687,10 @@ export class LspManager {
1539
1687
  }
1540
1688
 
1541
1689
  const warmed = await this.warmWorkspaceSymbolProjectsUntilResult({
1542
- findWarmTargets: findWorkspaceSymbolWarmTargets,
1690
+ findWarmTargets: (root, rootMarkers, fileTypes) =>
1691
+ findWorkspaceSymbolWarmTargets(root, rootMarkers, fileTypes, {
1692
+ policy: this.automaticPathPolicy,
1693
+ }),
1543
1694
  getWarmPosition: getWorkspaceSymbolWarmPosition,
1544
1695
  collect,
1545
1696
  routeMatches,
@@ -1637,28 +1788,21 @@ export class LspManager {
1637
1788
  for (const target of warmTargets) {
1638
1789
  const projectKey = this.workspaceSymbolProjectKey(client.name, target.projectRoot);
1639
1790
  if (this.warmedWorkspaceSymbolProjects.has(projectKey)) continue;
1640
- if (this.hasOpenFileInProject(client, target.projectRoot)) {
1641
- this.warmedWorkspaceSymbolProjects.add(projectKey);
1642
- continue;
1643
- }
1644
1791
 
1645
1792
  const openedClient = await this.ensureFileOpen(target.file);
1646
1793
  if (!openedClient) continue;
1647
1794
 
1648
1795
  // A cancelled pass must not mark projects warmed on its way out.
1649
1796
  throwIfCodeRequestInterrupted(control);
1650
- this.warmedWorkspaceSymbolProjects.add(projectKey);
1651
- warmedAny = true;
1652
-
1653
- const symbols = control
1654
- ? await openedClient.documentSymbols(target.file, control)
1655
- : await openedClient.documentSymbols(target.file);
1656
- if (symbols.kind !== "unavailable") {
1657
- const hoverPosition = getWarmPosition(symbols.data);
1658
- if (hoverPosition) {
1659
- if (control) await openedClient.hover(target.file, hoverPosition, control);
1660
- else await openedClient.hover(target.file, hoverPosition);
1661
- }
1797
+ const available = await this.probeDocumentSymbols(
1798
+ openedClient,
1799
+ target.file,
1800
+ getWarmPosition,
1801
+ control,
1802
+ );
1803
+ if (available) {
1804
+ this.warmedWorkspaceSymbolProjects.add(projectKey);
1805
+ warmedAny = true;
1662
1806
  }
1663
1807
 
1664
1808
  const collected = await collect(this.clients.values(), query);
@@ -1703,35 +1847,56 @@ export class LspManager {
1703
1847
  try {
1704
1848
  await raceRequestControl(probe, control);
1705
1849
  } finally {
1706
- this.pendingWarmProbes.delete(projectKey);
1850
+ if (this.pendingWarmProbes.get(projectKey) === probe) {
1851
+ this.pendingWarmProbes.delete(projectKey);
1852
+ }
1707
1853
  }
1708
1854
  }
1709
1855
 
1710
1856
  private async performWarmProbe(
1711
- _client: LspClient,
1857
+ client: LspClient,
1712
1858
  resolvedFile: string,
1713
1859
  projectKey: string,
1714
1860
  control?: CodeRequestControl,
1715
1861
  ): Promise<void> {
1862
+ const key = clientKey(client.name, client.root);
1863
+ if (this.clients.get(key) !== client) return;
1716
1864
  const openedClient = await this.ensureFileOpen(resolvedFile);
1717
- if (!openedClient) return;
1718
-
1719
- this.warmedSemanticProjects.add(projectKey);
1865
+ if (!openedClient || openedClient !== client || this.clients.get(key) !== client) return;
1720
1866
 
1721
1867
  // The probe is shared state for concurrent callers. One caller's
1722
1868
  // cancellation must not cancel the shared in-flight queries, so only the
1723
1869
  // Debug Operation ID is forwarded; each caller races its own wait above.
1724
1870
  const sharedControl = { operationId: control?.operationId };
1725
- const symbols = await openedClient.documentSymbols(resolvedFile, sharedControl);
1726
- if (symbols.kind === "unavailable") return;
1727
- const hoverPosition = getWorkspaceSymbolWarmPosition(symbols.data);
1728
- if (hoverPosition) {
1729
- await openedClient.hover(resolvedFile, hoverPosition, sharedControl);
1871
+ const available = await this.probeDocumentSymbols(
1872
+ openedClient,
1873
+ resolvedFile,
1874
+ getWorkspaceSymbolWarmPosition,
1875
+ sharedControl,
1876
+ );
1877
+ if (available && this.clients.get(key) === client) {
1878
+ this.warmedSemanticProjects.add(projectKey);
1730
1879
  }
1731
1880
  }
1732
1881
 
1733
- private hasOpenFileInProject(client: LspClient, projectRoot: string): boolean {
1734
- return client.openFiles.some((openFile) => projectRoots.isWithinOrEqual(projectRoot, openFile));
1882
+ private async probeDocumentSymbols(
1883
+ client: LspClient,
1884
+ file: string,
1885
+ getWarmPosition: (
1886
+ symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
1887
+ ) => import("../config/types.ts").Position | null,
1888
+ control?: CodeRequestControl,
1889
+ ): Promise<boolean> {
1890
+ const symbols = control
1891
+ ? await client.documentSymbols(file, control)
1892
+ : await client.documentSymbols(file);
1893
+ if (symbols.kind === "unavailable") return false;
1894
+ const hoverPosition = getWarmPosition(symbols.data);
1895
+ if (hoverPosition) {
1896
+ if (control) await client.hover(file, hoverPosition, control);
1897
+ else await client.hover(file, hoverPosition);
1898
+ }
1899
+ return true;
1735
1900
  }
1736
1901
 
1737
1902
  private workspaceSymbolProjectKey(serverName: string, projectRoot: string): string {
@@ -1766,6 +1931,17 @@ export class LspManager {
1766
1931
  }
1767
1932
  }
1768
1933
 
1934
+ function invalidateProjectConfigCaches(changes: readonly FileEvent[]): void {
1935
+ for (const change of changes) {
1936
+ const configPath = uriToFile(change.uri);
1937
+ if (!isProjectConfigFileName(path.basename(configPath))) continue;
1938
+ invalidateTsconfigCacheForConfig(configPath);
1939
+ if (change.type === FileChangeType.Created) {
1940
+ invalidateTsconfigCacheForConfigDir(path.dirname(configPath));
1941
+ }
1942
+ }
1943
+ }
1944
+
1769
1945
  function collectClientDiagnosticEvidenceDocuments(
1770
1946
  snapshots: Iterable<ClientDiagnosticSnapshot>,
1771
1947
  cwd: string,
@@ -1781,14 +1957,14 @@ function collectClientDiagnosticEvidenceDocuments(
1781
1957
  function collectOutstandingDiagnosticSummaryEntries(
1782
1958
  snapshots: Iterable<ClientDiagnosticSnapshot>,
1783
1959
  cwd: string,
1784
- excludePatterns: string[],
1960
+ includeFile: (file: string) => boolean,
1785
1961
  maxSeverity: number,
1786
1962
  ): OutstandingDiagnosticSummaryEntry[] {
1787
1963
  const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
1788
1964
  for (const snapshot of snapshots) {
1789
1965
  for (const entry of snapshot.entries) {
1790
1966
  const file = relativeFilePathFromUri(entry.uri, cwd);
1791
- if (shouldIgnoreLspPath(file, cwd) || isExcludedByPattern(file, excludePatterns)) continue;
1967
+ if (!includeFile(file)) continue;
1792
1968
  const existing = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
1793
1969
  const next = accumulateOutstandingDiagnostics(existing, entry.diagnostics, maxSeverity);
1794
1970
  if (next.total > 0) fileDiags.set(file, next);