@mrclrchtr/supi-code-intelligence 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +2 -2
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  5. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  6. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-lsp/README.md +34 -3
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  15. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  16. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +67 -28
  18. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +63 -6
  19. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +46 -5
  20. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +1 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +278 -79
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +29 -8
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +101 -19
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +19 -1
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +49 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +73 -15
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +1 -1
  29. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +2 -2
  30. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
  31. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
  35. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +4 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
  37. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -3
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +65 -17
  39. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +143 -21
  40. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +2 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -1
  42. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -0
  43. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  44. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  45. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  47. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  48. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  49. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  50. package/package.json +5 -5
  51. package/src/analysis/health/diagnostics.ts +58 -126
  52. package/src/analysis/health/file-scope.ts +184 -0
  53. package/src/analysis/health/recovery.ts +8 -1
  54. package/src/analysis/readiness.ts +4 -0
  55. package/src/analysis/search/ast-scan.ts +4 -1
  56. package/src/analysis/search/pattern-analysis.ts +10 -3
  57. package/src/analysis/search/pattern.ts +6 -1
  58. package/src/config.ts +35 -1
  59. package/src/extension.ts +57 -14
  60. package/src/overview/overview-data.ts +9 -2
  61. package/src/overview/overview.ts +49 -30
  62. package/src/overview/types.ts +2 -0
  63. package/src/session/find-types.ts +2 -2
  64. package/src/session/find-workflow.ts +3 -2
  65. package/src/session/health-refresh.ts +76 -49
  66. package/src/session/health-types.ts +7 -1
  67. package/src/session/health-workflow.ts +35 -18
  68. package/src/session/input/workflows.ts +2 -2
  69. package/src/session/inspect-workflow.ts +2 -1
  70. package/src/session/orientation/collect.ts +2 -2
  71. package/src/session/orientation-types.ts +2 -0
  72. package/src/session/orientation-workflow.ts +8 -2
  73. package/src/session/refactor-workflow.ts +2 -1
  74. package/src/session/session.ts +25 -1
  75. package/src/substrate/lsp/maintenance.ts +105 -23
  76. package/src/substrate/lsp/recovery.ts +11 -5
  77. package/src/tool/code_find/execute.ts +23 -0
  78. package/src/tool/code_find/guidance.ts +8 -0
  79. package/src/tool/{find → code_find}/render.ts +1 -1
  80. package/src/tool/{result/find.ts → code_find/result.ts} +36 -11
  81. package/src/tool/code_find/spec.ts +37 -0
  82. package/src/tool/code_graph/execute.ts +24 -0
  83. package/src/tool/code_graph/guidance.ts +6 -0
  84. package/src/tool/{graph → code_graph}/markdown.ts +1 -1
  85. package/src/tool/{result/graph.ts → code_graph/result.ts} +69 -4
  86. package/src/tool/code_graph/spec.ts +39 -0
  87. package/src/tool/{health → code_health}/execute.ts +2 -18
  88. package/src/tool/code_health/guidance.ts +8 -0
  89. package/src/tool/{health → code_health}/markdown.ts +68 -18
  90. package/src/tool/{result/health.ts → code_health/result.ts} +29 -4
  91. package/src/tool/code_health/spec.ts +41 -0
  92. package/src/tool/code_inspect/execute.ts +20 -0
  93. package/src/tool/code_inspect/guidance.ts +6 -0
  94. package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
  95. package/src/tool/{result/inspect.ts → code_inspect/result.ts} +32 -3
  96. package/src/tool/code_inspect/spec.ts +25 -0
  97. package/src/tool/code_orientation/execute.ts +25 -0
  98. package/src/tool/code_orientation/guidance.ts +9 -0
  99. package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
  100. package/src/tool/{result/orientation.ts → code_orientation/result.ts} +67 -3
  101. package/src/tool/code_orientation/spec.ts +25 -0
  102. package/src/tool/code_refactor_apply/execute.ts +17 -0
  103. package/src/tool/code_refactor_apply/guidance.ts +5 -0
  104. package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +5 -11
  105. package/src/tool/code_refactor_apply/spec.ts +26 -0
  106. package/src/tool/code_refactor_plan/execute.ts +27 -0
  107. package/src/tool/code_refactor_plan/guidance.ts +8 -0
  108. package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +9 -22
  109. package/src/tool/code_refactor_plan/spec.ts +25 -0
  110. package/src/tool/code_resolve/execute.ts +19 -0
  111. package/src/tool/code_resolve/guidance.ts +8 -0
  112. package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
  113. package/src/tool/{result/resolve.ts → code_resolve/result.ts} +37 -24
  114. package/src/tool/code_resolve/spec.ts +25 -0
  115. package/src/tool/guidance.ts +71 -52
  116. package/src/tool/infra/truncate.ts +40 -0
  117. package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
  118. package/src/tool/register.ts +9 -97
  119. package/src/tool/result/refactor.ts +1 -4
  120. package/src/tool/schemas.ts +26 -157
  121. package/src/tool/specs.ts +40 -126
  122. package/src/tool/find/execute.ts +0 -43
  123. package/src/tool/graph/execute.ts +0 -82
  124. package/src/tool/inspect/execute.ts +0 -40
  125. package/src/tool/orientation/execute.ts +0 -80
  126. package/src/tool/resolve/execute.ts +0 -53
  127. /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
  128. /package/src/tool/{find → code_find}/markdown.ts +0 -0
  129. /package/src/tool/{find → code_find}/modes.ts +0 -0
  130. /package/src/tool/{find → code_find}/tui.ts +0 -0
  131. /package/src/tool/{graph → code_graph}/tui.ts +0 -0
  132. /package/src/tool/{health → code_health}/refresh-status.ts +0 -0
  133. /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
  134. /package/src/tool/{health → code_health}/tui.ts +0 -0
  135. /package/src/tool/{inspect → code_inspect}/tui.ts +0 -0
  136. /package/src/tool/{orientation → code_orientation}/tui.ts +0 -0
  137. /package/src/tool/{refactor-apply → code_refactor_apply}/tui.ts +0 -0
  138. /package/src/tool/{refactor-plan → code_refactor_plan}/tui.ts +0 -0
  139. /package/src/tool/{resolve → code_resolve}/tui.ts +0 -0
@@ -10,13 +10,13 @@ import type { Diagnostic, FileEvent } from "../config/types.ts";
10
10
  import {
11
11
  type DiagnosticEvidenceDocument,
12
12
  type DiagnosticEvidenceSummary,
13
- emptyDiagnosticEvidence,
14
13
  summarizeDiagnosticEvidence,
15
14
  } from "../diagnostics/evidence.ts";
16
15
  import {
17
16
  assessStaleDiagnostics,
18
17
  type StaleDiagnosticAssessment,
19
18
  } from "../diagnostics/stale-diagnostics.ts";
19
+ import type { WorkspaceDiagnosticReport } from "../session/runtime-diagnostics.ts";
20
20
 
21
21
  export interface WorkspaceRecoveryResult {
22
22
  /** Active clients targeted by the best-effort refresh, not confirmed successful refreshes. */
@@ -28,8 +28,10 @@ export interface WorkspaceRecoveryResult {
28
28
  restartedServers: string[];
29
29
  /** Stall signal of the first restarted route, when this pass restarted a client. */
30
30
  restartReason?: RecoveryRestartReason;
31
- /** Final document-level evidence from the last refresh in this recovery pass. */
31
+ /** Final document-level evidence from this pass, starting from the caller's initial evidence when one was supplied. */
32
32
  diagnosticEvidence: DiagnosticEvidenceSummary;
33
+ /** Final diagnostic report captured after all refresh and recovery work. */
34
+ diagnosticReport: WorkspaceDiagnosticReport;
33
35
  /** Failure from the first refresh, when no later pass replaced it. */
34
36
  refreshFailureReason?: string;
35
37
  /** Wall-clock duration of the whole recovery pass, for telemetry. */
@@ -62,6 +64,7 @@ export interface WorkspaceRecoveryHost {
62
64
  isDiagnosticFile(filePath: string): boolean;
63
65
  getClientDiagnosticRoutes(): WorkspaceDiagnosticRoute[];
64
66
  getDiagnosticEvidence(): DiagnosticEvidenceSummary;
67
+ getWorkspaceDiagnosticReport(maxSeverity?: number): WorkspaceDiagnosticReport;
65
68
  getCwd(): string;
66
69
  restartClientsForFiles(
67
70
  filePaths: string[],
@@ -82,6 +85,13 @@ export function softRecoverWorkspaceDiagnostics(
82
85
  /**
83
86
  * Run a recovery pass, refreshing diagnostics and escalating if stale state remains.
84
87
  *
88
+ * When the caller already ran a refresh this pass, it can hand its evidence in
89
+ * `initialEvidence`; the recovery pass then skips its own initial refresh and
90
+ * still runs stale assessment and restart escalation on the current state.
91
+ * Caller evidence is only reused when the pass applies no watched-file changes:
92
+ * evidence captured before a change would otherwise be reported as current
93
+ * after it.
94
+ *
85
95
  * The pass observes request cancellation between its phases and propagates
86
96
  * the interruption as a rejection instead of swallowing it. A pass that
87
97
  * starts already cancelled rejects before any client or evidence work.
@@ -93,6 +103,8 @@ export async function recoverWorkspaceDiagnostics(
93
103
  restartIfStillStale?: boolean;
94
104
  maxWaitMs?: number;
95
105
  quietMs?: number;
106
+ /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
107
+ initialEvidence?: DiagnosticEvidenceSummary;
96
108
  control?: CodeRequestControl;
97
109
  } = {},
98
110
  ): Promise<WorkspaceRecoveryResult> {
@@ -100,22 +112,11 @@ export async function recoverWorkspaceDiagnostics(
100
112
  // evidence work may start for a pass the caller no longer awaits.
101
113
  throwIfCodeRequestInterrupted(options.control);
102
114
  const recoveryStartedAt = Date.now();
103
- const attemptedClients = softRecoverWorkspaceDiagnostics(host, options.changes ?? []);
115
+ const initial = await collectInitialEvidence(host, options);
116
+ const attemptedClients = initial.attemptedClients;
104
117
  const attemptedServers = host.getRunningClientNames();
105
- let diagnosticEvidence = emptyDiagnosticEvidence();
106
- let refreshFailureReason: string | undefined;
107
-
108
- try {
109
- diagnosticEvidence = await host.refreshOpenDiagnostics({
110
- maxWaitMs: options.maxWaitMs,
111
- quietMs: options.quietMs,
112
- ...options.control,
113
- });
114
- } catch (error) {
115
- if (isCodeRequestInterruption(error, options.control)) throw error;
116
- refreshFailureReason = errorMessage(error);
117
- diagnosticEvidence = host.getDiagnosticEvidence();
118
- }
118
+ let diagnosticEvidence = initial.diagnosticEvidence;
119
+ let refreshFailureReason = initial.refreshFailureReason;
119
120
 
120
121
  let staleAssessment = assessStaleDiagnostics(host.getOutstandingDiagnostics(1));
121
122
  let restartedClients = 0;
@@ -148,6 +149,7 @@ export async function recoverWorkspaceDiagnostics(
148
149
  // refresh-only pass must not report a clean result after the caller
149
150
  // cancelled the request.
150
151
  throwIfCodeRequestInterrupted(options.control);
152
+ const diagnosticReport = host.getWorkspaceDiagnosticReport(2);
151
153
 
152
154
  return recoveryResult();
153
155
 
@@ -159,6 +161,7 @@ export async function recoverWorkspaceDiagnostics(
159
161
  restartedServers: restartServerNames,
160
162
  ...(restartReason ? { restartReason } : {}),
161
163
  diagnosticEvidence,
164
+ diagnosticReport,
162
165
  ...(refreshFailureReason ? { refreshFailureReason } : {}),
163
166
  elapsedMs: Date.now() - recoveryStartedAt,
164
167
  staleAssessment,
@@ -173,6 +176,51 @@ interface RecoveryRestartTarget {
173
176
  files: string[];
174
177
  }
175
178
 
179
+ /** Establish the pass's starting evidence: caller-supplied reuse or a fresh refresh. */
180
+ async function collectInitialEvidence(
181
+ host: WorkspaceRecoveryHost,
182
+ options: {
183
+ changes?: FileEvent[];
184
+ initialEvidence?: DiagnosticEvidenceSummary;
185
+ maxWaitMs?: number;
186
+ quietMs?: number;
187
+ control?: CodeRequestControl;
188
+ },
189
+ ): Promise<{
190
+ attemptedClients: number;
191
+ diagnosticEvidence: DiagnosticEvidenceSummary;
192
+ refreshFailureReason?: string;
193
+ }> {
194
+ const changes = options.changes ?? [];
195
+ if (options.initialEvidence !== undefined && changes.length === 0) {
196
+ // Reuse skips the soft invalidation: the caller already refreshed this
197
+ // pass, and clearing cached pull result IDs without a refresh only forces
198
+ // a full pull on a later pass.
199
+ return {
200
+ attemptedClients: host.getRunningClientCount(),
201
+ diagnosticEvidence: options.initialEvidence,
202
+ };
203
+ }
204
+ const attemptedClients = softRecoverWorkspaceDiagnostics(host, changes);
205
+ try {
206
+ return {
207
+ attemptedClients,
208
+ diagnosticEvidence: await host.refreshOpenDiagnostics({
209
+ maxWaitMs: options.maxWaitMs,
210
+ quietMs: options.quietMs,
211
+ ...options.control,
212
+ }),
213
+ };
214
+ } catch (error) {
215
+ if (isCodeRequestInterruption(error, options.control)) throw error;
216
+ return {
217
+ attemptedClients,
218
+ diagnosticEvidence: host.getDiagnosticEvidence(),
219
+ refreshFailureReason: errorMessage(error),
220
+ };
221
+ }
222
+ }
223
+
176
224
  /**
177
225
  * Collect restart targets: push-only routes with a protocol-stall signal.
178
226
  *
@@ -10,16 +10,19 @@ import {
10
10
  unavailableCodeQuery,
11
11
  } from "@mrclrchtr/supi-code-runtime/api";
12
12
  import * as projectRoots from "@mrclrchtr/supi-core/project";
13
+ import * as ts from "typescript";
13
14
  import {
14
15
  LspClient,
15
16
  type LspClientLifecycleTransitionKind,
16
17
  RECOVERY_CLIENT_STARTUP_BOUND_MS,
17
18
  withTimeout,
18
19
  } from "../client/client.ts";
20
+ import type { ClientDiagnosticSnapshot } from "../client/client-document-state.ts";
19
21
 
20
22
  export { RECOVERY_CLIENT_STARTUP_BOUND_MS } from "../client/client.ts";
21
23
 
22
24
  import { getServerForFile } from "../config/config.ts";
25
+ import { getFileScopeDecision } from "../config/tsconfig-scope.ts";
23
26
  import type {
24
27
  DetectedProjectServer,
25
28
  Diagnostic,
@@ -42,6 +45,11 @@ import {
42
45
  summarizeDiagnosticEvidence,
43
46
  } from "../diagnostics/evidence.ts";
44
47
  import { raceRequestControl } from "../session/readiness.ts";
48
+ import type {
49
+ ScopeDecisionEntry,
50
+ ScopeDecisionSummary,
51
+ WorkspaceDiagnosticReport,
52
+ } from "../session/runtime-diagnostics.ts";
45
53
  import {
46
54
  displayRelativeFilePath,
47
55
  formatCoverageSummaryText,
@@ -89,6 +97,35 @@ import {
89
97
 
90
98
  type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
91
99
 
100
+ /** Maximum tracked-file entries retained in one scope-decision telemetry summary. */
101
+ const SCOPE_DECISION_MAX_ENTRIES = 24;
102
+
103
+ interface ScopeDecisionAccumulator {
104
+ entries: ScopeDecisionEntry[];
105
+ counts: ScopeDecisionSummary["counts"];
106
+ basisCounts: Record<string, number>;
107
+ }
108
+
109
+ function accumulateScopeDecision(
110
+ accumulator: ScopeDecisionAccumulator,
111
+ file: string,
112
+ decision: ReturnType<typeof getFileScopeDecision>,
113
+ ): void {
114
+ const statusKey =
115
+ decision.status === "no-config"
116
+ ? "noConfig"
117
+ : decision.status === "out-of-tree"
118
+ ? "outOfTree"
119
+ : decision.status;
120
+ accumulator.counts[statusKey]++;
121
+ if (decision.basis) {
122
+ accumulator.basisCounts[decision.basis] = (accumulator.basisCounts[decision.basis] ?? 0) + 1;
123
+ }
124
+ if (accumulator.entries.length < SCOPE_DECISION_MAX_ENTRIES) {
125
+ accumulator.entries.push({ file, status: decision.status, basis: decision.basis });
126
+ }
127
+ }
128
+
92
129
  type FileRoute = {
93
130
  serverName: string;
94
131
  serverConfig: ServerConfig;
@@ -639,6 +676,8 @@ export class LspManager {
639
676
  restartIfStillStale?: boolean;
640
677
  maxWaitMs?: number;
641
678
  quietMs?: number;
679
+ /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
680
+ initialEvidence?: DiagnosticEvidenceSummary;
642
681
  control?: CodeRequestControl;
643
682
  }): Promise<WorkspaceRecoveryResult> {
644
683
  return recoverWorkspaceDiagnosticsImpl(this, options);
@@ -767,6 +806,55 @@ export class LspManager {
767
806
  .filter((entry) => entry.openFiles.length > 0);
768
807
  return formatCoverageSummaryText(relevantEntries, maxServers, maxFiles);
769
808
  }
809
+ /**
810
+ * Capture one coherent diagnostic report from the current client snapshots.
811
+ *
812
+ * Summary and detailed projections share the same cache observation so a
813
+ * caller cannot combine entries from one generation with evidence from
814
+ * another.
815
+ */
816
+ getWorkspaceDiagnosticReport(maxSeverity: number = 2): WorkspaceDiagnosticReport {
817
+ this.pruneMissingFiles();
818
+ const snapshots = Array.from(this.clients.values()).map((client) =>
819
+ client.getDiagnosticSnapshot(),
820
+ );
821
+ const evidence = this.toWorkspaceDiagnosticEvidence(
822
+ snapshots.flatMap((snapshot) =>
823
+ snapshot.documents.map((document) => ({
824
+ file: relativeFilePathFromUri(document.uri, this.cwd),
825
+ status: document.status,
826
+ })),
827
+ ),
828
+ );
829
+ const current =
830
+ snapshots.every((snapshot) => snapshot.current) &&
831
+ evidence.documents.every((document) => document.status === "confirmed");
832
+ const fileDiags = new Map<string, { errors: number; warnings: number }>();
833
+ for (const snapshot of snapshots) {
834
+ for (const entry of snapshot.entries) {
835
+ collectDiagnosticSummaryCounts(fileDiags, entry, this.cwd, this.excludePatterns);
836
+ }
837
+ }
838
+
839
+ return {
840
+ summary: {
841
+ entries: Array.from(fileDiags.entries()).map(([file, counts]) => ({ file, ...counts })),
842
+ current,
843
+ evidence,
844
+ },
845
+ outstanding: {
846
+ entries: collectOutstandingDiagnosticsDetailed(
847
+ snapshots.map((snapshot) => snapshot.entries),
848
+ this.cwd,
849
+ this.excludePatterns,
850
+ maxSeverity,
851
+ ),
852
+ current,
853
+ evidence,
854
+ },
855
+ };
856
+ }
857
+
770
858
  /** Get a diagnostic summary across all servers and files. */
771
859
  getDiagnosticSummary(): DiagnosticSummary[] {
772
860
  return this.getDiagnosticSnapshot().entries;
@@ -811,15 +899,16 @@ export class LspManager {
811
899
  evidence: DiagnosticEvidenceSummary;
812
900
  } {
813
901
  this.pruneMissingFiles();
814
- const evidence = this.toWorkspaceDiagnosticEvidence(
815
- collectClientDiagnosticEvidenceDocuments(this.clients.values(), this.cwd),
902
+ const snapshots = Array.from(this.clients.values()).map((client) =>
903
+ client.getDiagnosticSnapshot(),
816
904
  );
817
- const clientCurrent = Array.from(this.clients.values()).every(
818
- (client) => client.getDiagnosticSnapshot().current,
905
+ const evidence = this.toWorkspaceDiagnosticEvidence(
906
+ collectClientDiagnosticEvidenceDocuments(snapshots, this.cwd),
819
907
  );
908
+ const clientCurrent = snapshots.every((snapshot) => snapshot.current);
820
909
  return {
821
910
  entries: collectOutstandingDiagnosticSummaryEntries(
822
- this.clients.values(),
911
+ snapshots,
823
912
  this.cwd,
824
913
  this.excludePatterns,
825
914
  maxSeverity,
@@ -854,29 +943,62 @@ export class LspManager {
854
943
  evidence: DiagnosticEvidenceSummary;
855
944
  } {
856
945
  this.pruneMissingFiles();
857
- const clients = Array.from(this.clients.values());
858
- let clientCurrent = true;
859
- const evidenceDocuments = clients.flatMap((client) => {
860
- const snapshot = client.getDiagnosticSnapshot();
861
- clientCurrent &&= snapshot.current;
862
- return snapshot.documents.map((document) => ({
946
+ const snapshots = Array.from(this.clients.values()).map((client) =>
947
+ client.getDiagnosticSnapshot(),
948
+ );
949
+ const evidenceDocuments = snapshots.flatMap((snapshot) =>
950
+ snapshot.documents.map((document) => ({
863
951
  file: relativeFilePathFromUri(document.uri, this.cwd),
864
952
  status: document.status,
865
- }));
866
- });
953
+ })),
954
+ );
867
955
  const evidence = this.toWorkspaceDiagnosticEvidence(evidenceDocuments);
868
956
  return {
869
957
  entries: collectOutstandingDiagnosticsDetailed(
870
- clients,
958
+ snapshots.map((snapshot) => snapshot.entries),
871
959
  this.cwd,
872
960
  this.excludePatterns,
873
961
  maxSeverity,
874
962
  ),
875
963
  current:
876
- clientCurrent && evidence.documents.every((document) => document.status === "confirmed"),
964
+ snapshots.every((snapshot) => snapshot.current) &&
965
+ evidence.documents.every((document) => document.status === "confirmed"),
877
966
  evidence,
878
967
  };
879
968
  }
969
+
970
+ /**
971
+ * Aggregate the tsconfig scope decision for every tracked file.
972
+ *
973
+ * The recovery telemetry surface uses this to report why tracked files are
974
+ * in or out of scope without re-deriving the scope logic. The returned
975
+ * entry list is bounded; counts are exact.
976
+ */
977
+ getScopeDecisionSummary(): ScopeDecisionSummary {
978
+ this.pruneMissingFiles();
979
+ const accumulator: ScopeDecisionAccumulator = {
980
+ entries: [],
981
+ counts: { included: 0, excluded: 0, noConfig: 0, outOfTree: 0 },
982
+ basisCounts: {},
983
+ };
984
+ let totalFiles = 0;
985
+
986
+ for (const client of this.clients.values()) {
987
+ for (const document of client.getDiagnosticSnapshot().documents) {
988
+ const file = relativeFilePathFromUri(document.uri, this.cwd);
989
+ totalFiles++;
990
+ accumulateScopeDecision(accumulator, file, getFileScopeDecision(file, this.cwd));
991
+ }
992
+ }
993
+
994
+ return {
995
+ caseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
996
+ counts: accumulator.counts,
997
+ basisCounts: accumulator.basisCounts,
998
+ entries: accumulator.entries,
999
+ totalFiles,
1000
+ };
1001
+ }
880
1002
  private toWorkspaceDiagnosticEvidence(
881
1003
  documents: readonly DiagnosticEvidenceDocument[],
882
1004
  ): DiagnosticEvidenceSummary {
@@ -1089,11 +1211,11 @@ export class LspManager {
1089
1211
  }
1090
1212
 
1091
1213
  function collectClientDiagnosticEvidenceDocuments(
1092
- clients: Iterable<LspClient>,
1214
+ snapshots: Iterable<ClientDiagnosticSnapshot>,
1093
1215
  cwd: string,
1094
1216
  ): DiagnosticEvidenceDocument[] {
1095
- return Array.from(clients).flatMap((client) =>
1096
- client.getDiagnosticSnapshot().documents.map((document) => ({
1217
+ return Array.from(snapshots).flatMap((snapshot) =>
1218
+ snapshot.documents.map((document) => ({
1097
1219
  file: relativeFilePathFromUri(document.uri, cwd),
1098
1220
  status: document.status,
1099
1221
  })),
@@ -1101,14 +1223,14 @@ function collectClientDiagnosticEvidenceDocuments(
1101
1223
  }
1102
1224
 
1103
1225
  function collectOutstandingDiagnosticSummaryEntries(
1104
- clients: Iterable<LspClient>,
1226
+ snapshots: Iterable<ClientDiagnosticSnapshot>,
1105
1227
  cwd: string,
1106
1228
  excludePatterns: string[],
1107
1229
  maxSeverity: number,
1108
1230
  ): OutstandingDiagnosticSummaryEntry[] {
1109
1231
  const fileDiags = new Map<string, OutstandingDiagnosticSummaryEntry>();
1110
- for (const client of clients) {
1111
- for (const entry of client.getDiagnosticSnapshot().entries) {
1232
+ for (const snapshot of snapshots) {
1233
+ for (const entry of snapshot.entries) {
1112
1234
  const file = relativeFilePathFromUri(entry.uri, cwd);
1113
1235
  if (shouldIgnoreLspPath(file, cwd) || isExcludedByPattern(file, excludePatterns)) continue;
1114
1236
  const existing = fileDiags.get(file) ?? createOutstandingDiagnosticSummary(file);
@@ -35,6 +35,8 @@ export interface WorkspaceLspDiagnosticSurface {
35
35
  restartIfStillStale?: boolean;
36
36
  maxWaitMs?: number;
37
37
  quietMs?: number;
38
+ /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
39
+ initialEvidence?: DiagnosticEvidenceSummary;
38
40
  control?: CodeRequestControl;
39
41
  }): Promise<RecoverDiagnosticsResult>;
40
42
  }
@@ -21,6 +21,20 @@ export interface WorkspaceDiagnosticSummaryEntry {
21
21
  warnings: number;
22
22
  }
23
23
 
24
+ /**
25
+ * One coherent post-refresh observation of workspace diagnostics.
26
+ *
27
+ * Both projections are captured from the same client snapshots. The report is
28
+ * short-lived and must not be retained as the runtime's last refresh attempt.
29
+ */
30
+ export interface WorkspaceDiagnosticReport {
31
+ summary: WorkspaceDiagnosticSnapshot<WorkspaceDiagnosticSummaryEntry>;
32
+ outstanding: WorkspaceDiagnosticSnapshot<{
33
+ file: string;
34
+ diagnostics: Diagnostic[];
35
+ }>;
36
+ }
37
+
24
38
  /** Outstanding diagnostics grouped by file, including info and hint counts. */
25
39
  export interface OutstandingDiagnosticSummaryEntry {
26
40
  file: string;
@@ -36,8 +50,10 @@ export interface RecoverDiagnosticsResult {
36
50
  /** Active clients targeted by the best-effort refresh, not confirmed successful refreshes. */
37
51
  attemptedClients: number;
38
52
  restartedClients: number;
39
- /** Final document-level evidence from the last refresh in this recovery pass. */
53
+ /** Evidence collected by the refresh and recovery operations. */
40
54
  diagnosticEvidence: DiagnosticEvidenceSummary;
55
+ /** Final diagnostic report captured after all refresh and recovery work. */
56
+ diagnosticReport: WorkspaceDiagnosticReport;
41
57
  /** Failure from the first refresh, when no later pass replaced it. */
42
58
  refreshFailureReason?: string;
43
59
  /** Wall-clock duration of the whole recovery pass, for telemetry. */
@@ -48,3 +64,29 @@ export interface RecoverDiagnosticsResult {
48
64
  warning: string | null;
49
65
  };
50
66
  }
67
+
68
+ /** One tracked file's tsconfig scope decision, for debug telemetry. */
69
+ export interface ScopeDecisionEntry {
70
+ /** Workspace-relative file path. */
71
+ file: string;
72
+ status: "included" | "excluded" | "no-config" | "out-of-tree";
73
+ /** Decision mechanism; null when no decision applies. */
74
+ basis: string | null;
75
+ }
76
+
77
+ /** Aggregate tsconfig scope decisions for all tracked files, for debug telemetry. */
78
+ export interface ScopeDecisionSummary {
79
+ caseSensitiveFileNames: boolean;
80
+ counts: {
81
+ included: number;
82
+ excluded: number;
83
+ noConfig: number;
84
+ outOfTree: number;
85
+ };
86
+ /** Exact decision-basis counts; keys are {@link ScopeDecisionEntry.basis} values. */
87
+ basisCounts: Record<string, number>;
88
+ /** Bounded workspace-relative entries (oldest first), not the full set. */
89
+ entries: ScopeDecisionEntry[];
90
+ /** Total number of tracked files the summary was computed over. */
91
+ totalFiles: number;
92
+ }
@@ -45,6 +45,7 @@ export type {
45
45
  DiagnosticEvidenceSummary,
46
46
  OutstandingDiagnosticSummaryEntry,
47
47
  RecoverDiagnosticsResult,
48
+ WorkspaceDiagnosticReport,
48
49
  WorkspaceDiagnosticSnapshot,
49
50
  WorkspaceDiagnosticSummaryEntry,
50
51
  } from "./runtime-diagnostics.ts";
@@ -63,6 +64,33 @@ function unavailableFileQuery<T>(operation: string, file: string): CodeQueryResu
63
64
  return unavailableCodeQuery(`No routed LSP client could complete ${operation} for ${file}.`);
64
65
  }
65
66
 
67
+ /** Emit one aggregate tsconfig scope-decision event after a recovery pass. */
68
+ function recordScopeDecisionEvent(manager: LspManager): void {
69
+ try {
70
+ const summary = manager.getScopeDecisionSummary();
71
+ recordDebugEvent({
72
+ source: "lsp",
73
+ level: "debug",
74
+ category: "diagnostics.scope",
75
+ message: "LSP diagnostic tsconfig scope decisions",
76
+ cwd: truncateIdentity(manager.getCwd()),
77
+ data: {
78
+ caseSensitiveFileNames: summary.caseSensitiveFileNames,
79
+ counts: summary.counts,
80
+ basisCounts: summary.basisCounts,
81
+ totalFiles: summary.totalFiles,
82
+ entries: summary.entries.map((entry) => ({
83
+ file: truncateIdentity(entry.file),
84
+ status: entry.status,
85
+ ...(entry.basis ? { basis: entry.basis } : {}),
86
+ })),
87
+ },
88
+ });
89
+ } catch {
90
+ // Telemetry must never turn a completed recovery into a failure.
91
+ }
92
+ }
93
+
66
94
  class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
67
95
  constructor(private readonly manager: LspManager) {}
68
96
 
@@ -315,6 +343,8 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
315
343
  restartIfStillStale?: boolean;
316
344
  maxWaitMs?: number;
317
345
  quietMs?: number;
346
+ /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
347
+ initialEvidence?: DiagnosticEvidenceSummary;
318
348
  control?: CodeRequestControl;
319
349
  }): Promise<RecoverDiagnosticsResult> {
320
350
  const recoveryStartedAt = Date.now();
@@ -337,6 +367,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
337
367
  ...(result.restartReason ? { reason: result.restartReason } : {}),
338
368
  },
339
369
  });
370
+ recordScopeDecisionEvent(this.manager);
340
371
  return result;
341
372
  } catch (error) {
342
373
  // A cancelled pass has no result object, so telemetry records the
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "./api": "./src/api.ts",
54
54
  "./config": "./src/config.ts",
55
55
  "./context": "./src/context.ts",
56
- "./debug": "./src/debug-registry.ts",
56
+ "./debug": "./src/debug.ts",
57
57
  "./evidence-badge": "./src/evidence-badge.ts",
58
58
  "./footer-registry": "./src/footer-registry.ts",
59
59
  "./llm": "./src/llm.ts",
@@ -11,7 +11,7 @@ export * from "./config.ts";
11
11
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
12
  export * from "./context.ts";
13
13
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
- export * from "./debug-registry.ts";
14
+ export * from "./debug.ts";
15
15
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
16
  export * from "./evidence-badge.ts";
17
17
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
@@ -4,9 +4,6 @@
4
4
  // supi-debug extension owns policy/configuration and exposes events through a
5
5
  // command/tool while this module stays dependency-free for producers.
6
6
 
7
- // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
8
- export * from "./debug-timing.ts";
9
-
10
7
  export type DebugLevel = "debug" | "info" | "warning" | "error";
11
8
  export type DebugAgentAccess = "off" | "sanitized" | "raw";
12
9
  export interface DebugRegistryConfig {
@@ -0,0 +1,9 @@
1
+ // Debug domain entry for `@mrclrchtr/supi-core/debug`.
2
+ //
3
+ // Kept separate from debug-registry.ts so debug-timing.ts can import the
4
+ // registry without creating an import cycle through the barrel re-export.
5
+
6
+ // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
7
+ export * from "./debug-registry.ts";
8
+ // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
9
+ export * from "./debug-timing.ts";
@@ -11,7 +11,7 @@ export * from "./config.ts";
11
11
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
12
  export * from "./context.ts";
13
13
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
- export * from "./debug-registry.ts";
14
+ export * from "./debug.ts";
15
15
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
16
  export * from "./footer-registry.ts";
17
17
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "5.0.0",
3
+ "version": "6.0.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": "5.0.0",
40
- "@mrclrchtr/supi-core": "5.0.0"
39
+ "@mrclrchtr/supi-core": "6.0.0",
40
+ "@mrclrchtr/supi-code-runtime": "6.0.0"
41
41
  },
42
42
  "bundledDependencies": [
43
43
  "@mrclrchtr/supi-code-runtime",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-intelligence",
3
- "version": "5.0.0",
3
+ "version": "6.0.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-core": "5.0.0",
39
- "@mrclrchtr/supi-lsp": "5.0.0",
40
- "@mrclrchtr/supi-tree-sitter": "5.0.0",
41
- "@mrclrchtr/supi-code-runtime": "5.0.0"
38
+ "@mrclrchtr/supi-code-runtime": "6.0.0",
39
+ "@mrclrchtr/supi-lsp": "6.0.0",
40
+ "@mrclrchtr/supi-tree-sitter": "6.0.0",
41
+ "@mrclrchtr/supi-core": "6.0.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",