@mrclrchtr/supi-code-intelligence 6.3.0 → 7.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 (136) hide show
  1. package/README.md +164 -82
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  4. package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  5. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  9. package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
  43. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +4 -32
  47. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sources.ts +108 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
  50. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +770 -56
  52. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
  53. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
  54. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +48 -12
  55. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +50 -4
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
  68. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  69. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  70. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  71. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  72. package/node_modules/yaml/package.json +1 -1
  73. package/package.json +5 -5
  74. package/src/analysis/health/diagnostics.ts +1 -2
  75. package/src/analysis/health/file-scope.ts +4 -1
  76. package/src/analysis/health/server-status.ts +1 -1
  77. package/src/analysis/relations/callees.ts +17 -8
  78. package/src/analysis/relations/implementations.ts +1 -2
  79. package/src/analysis/relations/references.ts +8 -4
  80. package/src/analysis/relations/types.ts +6 -30
  81. package/src/analysis/target/anchored.ts +9 -1
  82. package/src/analysis/target/symbol.ts +29 -11
  83. package/src/analysis/target/types.ts +14 -6
  84. package/src/api.ts +2 -0
  85. package/src/session/graph/collect.ts +18 -3
  86. package/src/session/health-refresh.ts +27 -11
  87. package/src/session/health-types.ts +21 -4
  88. package/src/session/health-workflow.ts +28 -22
  89. package/src/session/orientation-types.ts +5 -6
  90. package/src/session/orientation-workflow.ts +10 -2
  91. package/src/session/session.ts +12 -5
  92. package/src/session/target-workflow.ts +9 -6
  93. package/src/substrate/lsp/maintenance.ts +53 -47
  94. package/src/substrate/lsp/source-tracking.ts +252 -0
  95. package/src/tool/code_find/guidance.ts +3 -3
  96. package/src/tool/code_find/spec.ts +5 -4
  97. package/src/tool/code_find/tui.ts +7 -1
  98. package/src/tool/code_graph/details.ts +32 -0
  99. package/src/tool/code_graph/display.ts +54 -0
  100. package/src/tool/code_graph/format.ts +60 -0
  101. package/src/tool/code_graph/guidance.ts +2 -2
  102. package/src/tool/code_graph/markdown.ts +76 -198
  103. package/src/tool/code_graph/read-next.ts +65 -0
  104. package/src/tool/code_graph/result.ts +118 -83
  105. package/src/tool/code_graph/tui.ts +216 -54
  106. package/src/tool/code_health/guidance.ts +3 -5
  107. package/src/tool/code_health/markdown.ts +90 -57
  108. package/src/tool/code_health/refresh-outcome.ts +51 -0
  109. package/src/tool/code_health/refresh-status.ts +130 -43
  110. package/src/tool/code_health/scope.ts +78 -0
  111. package/src/tool/code_health/spec.ts +1 -1
  112. package/src/tool/code_health/tui.ts +29 -38
  113. package/src/tool/code_inspect/guidance.ts +5 -3
  114. package/src/tool/code_inspect/tui.ts +7 -1
  115. package/src/tool/code_orientation/guidance.ts +3 -4
  116. package/src/tool/code_orientation/result.ts +31 -8
  117. package/src/tool/code_orientation/tui.ts +26 -5
  118. package/src/tool/code_refactor_apply/guidance.ts +1 -1
  119. package/src/tool/code_refactor_apply/tui.ts +7 -3
  120. package/src/tool/code_refactor_plan/guidance.ts +3 -5
  121. package/src/tool/code_refactor_plan/tui.ts +7 -1
  122. package/src/tool/code_resolve/guidance.ts +3 -5
  123. package/src/tool/code_resolve/result.ts +13 -10
  124. package/src/tool/code_resolve/tui.ts +18 -1
  125. package/src/tool/guidance.ts +11 -9
  126. package/src/tool/infra/truncate.ts +39 -13
  127. package/src/tool/result/candidate-row.ts +27 -0
  128. package/src/tool/result/errors.ts +21 -6
  129. package/src/tool/result/types.ts +7 -3
  130. package/src/tool/schemas.ts +5 -0
  131. package/src/types/execution.ts +2 -0
  132. package/src/types/index.ts +2 -0
  133. package/src/ui/tui/candidate-legacy-body.ts +69 -0
  134. package/src/ui/tui/candidate-selection.ts +366 -0
  135. package/src/ui/tui/common.ts +26 -31
  136. package/src/ui/tui/execution-error.ts +92 -0
@@ -1,7 +1,8 @@
1
- // biome-ignore-all lint/style/noExcessiveLinesPerFile: refresh orchestration, pull collection, and the reopen fallback stay in one cohesive module.
1
+ // biome-ignore-all lint/style/noExcessiveLinesPerFile: refresh orchestration and evidence collection stay in one cohesive module.
2
2
  import { readFileSync } from "node:fs";
3
3
  import {
4
4
  type CodeRequestControl,
5
+ isCodeRequestDeadlineError,
5
6
  isCodeRequestInterruption,
6
7
  throwIfCodeRequestInterrupted,
7
8
  } from "@mrclrchtr/supi-code-runtime/api";
@@ -11,31 +12,23 @@ import {
11
12
  type DiagnosticEvidenceSummary,
12
13
  summarizeDiagnosticEvidence,
13
14
  } from "../diagnostics/evidence.ts";
14
- import { detectLanguageId } from "../utils.ts";
15
+ import { raceRequestControl } from "../session/readiness.ts";
15
16
  import {
16
17
  type DiagnosticCacheEntry,
17
18
  type DiagnosticSynchronization,
18
19
  hasCurrentEvidence,
19
20
  hasFreshEvidence,
20
- hasFreshPush,
21
- isCurrentSynchronization,
22
21
  latestCurrentEvidenceReceivedAt,
23
22
  nextDocumentVersion,
24
- raceDiagnosticPull,
25
23
  } from "./client-diagnostic-evidence.ts";
26
24
  import type { ClientDiagnosticsHost } from "./client-diagnostic-host.ts";
27
25
  import type {
28
26
  DiagnosticPublicationIdentity,
29
27
  DiagnosticPublicationSynchronization,
30
28
  } from "./client-diagnostic-publication.ts";
31
- import { pullDiagnosticEvidence } from "./client-diagnostic-pull.ts";
32
- import type { DiagnosticPullRequest } from "./client-diagnostic-request.ts";
33
- import {
34
- DiagnosticObserver,
35
- DiagnosticPullError,
36
- isDiagnosticTimeout,
37
- } from "./client-diagnostic-timing.ts";
38
- import type { DiagnosticStateWait, DiagnosticWaitRegistry } from "./client-diagnostic-waiters.ts";
29
+ import { isDiagnosticRequestInvalidated } from "./client-diagnostic-request.ts";
30
+ import { DiagnosticObserver, isDiagnosticTimeout } from "./client-diagnostic-timing.ts";
31
+ import type { DiagnosticWaitRegistry } from "./client-diagnostic-waiters.ts";
39
32
  import {
40
33
  fingerprintDocumentContent,
41
34
  hasConfirmedDiagnosticEvidence,
@@ -43,7 +36,6 @@ import {
43
36
  } from "./client-document-state.ts";
44
37
  import {
45
38
  type ResynchronizeDocumentsResult,
46
- reopenDocument,
47
39
  resynchronizeOpenDocuments,
48
40
  } from "./client-document-sync.ts";
49
41
  import { getDiagnosticFileState } from "./client-file-state.ts";
@@ -62,7 +54,7 @@ export function sendDidCloseNotification(
62
54
  export function buildDiagnosticRefreshEvidence(options: {
63
55
  requestedFiles: readonly string[];
64
56
  resynchronization: ResynchronizeDocumentsResult;
65
- /** Synchronizations that prove evidence, after reopen-resync updates. */
57
+ /** Synchronizations that can prove evidence after this refresh. */
66
58
  synchronizations: readonly DiagnosticSynchronization[];
67
59
  failedPullUris: ReadonlySet<string>;
68
60
  failedFiles: ReadonlySet<string>;
@@ -70,15 +62,12 @@ export function buildDiagnosticRefreshEvidence(options: {
70
62
  currentEvidenceRevision: number;
71
63
  openDocuments: ReadonlyMap<string, unknown>;
72
64
  diagnosticStore: ReadonlyMap<string, DiagnosticCacheEntry>;
73
- /** Required quiet time after the latest push for this refresh result. */
74
- pushQuietMs?: number;
75
65
  }): DiagnosticEvidenceSummary {
76
66
  const synchronizationByFile = new Map(
77
67
  options.synchronizations.map((item) => [uriToFile(item.uri), item]),
78
68
  );
79
69
  const removedFiles = new Set(options.resynchronization.removedFiles);
80
70
  const failedFiles = new Set(options.resynchronization.failedFiles);
81
- const observedAt = Date.now();
82
71
  const documents = options.requestedFiles.map((file) => {
83
72
  const uri = fileToUri(file);
84
73
  const synchronization = synchronizationByFile.get(file);
@@ -101,15 +90,27 @@ export function buildDiagnosticRefreshEvidence(options: {
101
90
  store: options.diagnosticStore,
102
91
  synchronization,
103
92
  currentEvidenceRevision: options.currentEvidenceRevision,
104
- pushQuietMs: options.pushQuietMs,
105
- observedAt,
106
93
  })
107
94
  ) {
108
95
  return { file, status: "confirmed" as const };
109
96
  }
97
+ const currentPushObservation = Boolean(
98
+ synchronization &&
99
+ options.diagnosticStore.get(uri)?.source === "push" &&
100
+ hasCurrentEvidence(
101
+ options.diagnosticStore,
102
+ synchronization,
103
+ options.currentEvidenceRevision,
104
+ ),
105
+ );
110
106
  return {
111
107
  file,
112
- status: options.failedPullUris.has(uri) ? ("failed" as const) : ("unconfirmed" as const),
108
+ // A failed request with a current ambient push remains unconfirmed, not
109
+ // failed: the push is useful observation but cannot confirm the file.
110
+ status:
111
+ options.failedPullUris.has(uri) && !currentPushObservation
112
+ ? ("failed" as const)
113
+ : ("unconfirmed" as const),
113
114
  };
114
115
  });
115
116
  return summarizeDiagnosticEvidence(documents);
@@ -120,135 +121,101 @@ function hasSettledRefreshEvidence(options: {
120
121
  store: ReadonlyMap<string, DiagnosticCacheEntry>;
121
122
  synchronization: DiagnosticSynchronization;
122
123
  currentEvidenceRevision: number;
123
- pushQuietMs: number | undefined;
124
- observedAt: number;
125
124
  }): boolean {
126
- if (!hasFreshEvidence(options.store, options.synchronization, options.currentEvidenceRevision)) {
127
- return false;
128
- }
129
- const entry = options.store.get(options.synchronization.uri);
130
- return (
131
- options.pushQuietMs === undefined ||
132
- entry?.source !== "push" ||
133
- options.observedAt - entry.receivedAt >= options.pushQuietMs
134
- );
135
- }
136
-
137
- /** Pull one document and reject evidence from a stale client generation. */
138
- export function pullClientDiagnosticEvidence(
139
- options: Omit<DiagnosticPullRequest, "previousResultId"> & {
140
- store: Map<string, DiagnosticCacheEntry>;
141
- synchronizationId?: number;
142
- evidenceRevision: number;
143
- currentRevision(): number;
144
- isCurrentSynchronization(): boolean;
145
- isRelatedUriTracked(uri: string): boolean;
146
- pull(
147
- request: DiagnosticPullRequest,
148
- ): Promise<import("../config/types.ts").DocumentDiagnosticReport | null>;
149
- },
150
- ): Promise<boolean> {
151
- return pullDiagnosticEvidence(options);
125
+ return hasFreshEvidence(options.store, options.synchronization, options.currentEvidenceRevision);
152
126
  }
153
127
 
154
- /** Pull one document through a client's host and preserve generation checks. */
155
- export function pullClientDiagnosticEvidenceFromHost(options: {
156
- host: Pick<ClientDiagnosticsHost, "pullDocumentDiagnostics">;
157
- store: Map<string, DiagnosticCacheEntry>;
158
- openDocuments: ReadonlyMap<string, { synchronizationId: number; evidenceRevision?: number }>;
159
- currentEvidenceRevision(): number;
160
- isRelatedUriTracked(uri: string): boolean;
161
- request: Omit<DiagnosticPullRequest, "previousResultId"> & {
162
- synchronizationId?: number;
163
- evidenceRevision?: number;
164
- };
165
- }): Promise<boolean> {
166
- return pullClientDiagnosticEvidence({
167
- store: options.store,
168
- ...options.request,
169
- evidenceRevision: options.request.evidenceRevision ?? options.currentEvidenceRevision(),
170
- currentRevision: options.currentEvidenceRevision,
171
- isCurrentSynchronization: () =>
172
- options.request.synchronizationId === undefined ||
173
- isCurrentSynchronization(options.openDocuments, {
174
- uri: options.request.uri,
175
- synchronizationId: options.request.synchronizationId,
176
- evidenceRevision: options.request.evidenceRevision,
177
- }),
178
- isRelatedUriTracked: options.isRelatedUriTracked,
179
- pull: (request) => options.host.pullDocumentDiagnostics(request),
180
- });
181
- }
182
-
183
- /** Collect pull evidence for every synchronized document in one refresh. */
184
- export async function pullDiagnosticsForOpenDocuments(options: {
185
- requests: readonly DiagnosticSynchronization[];
186
- syncStart: number;
187
- maxWaitMs: number;
188
- signal?: AbortSignal;
189
- deadline?: number;
190
- operationId?: string;
191
- currentEvidenceRevision: () => number;
192
- openDocuments: ReadonlyMap<string, { evidenceRevision: number; synchronizationId: number }>;
193
- diagnosticStore: ReadonlyMap<string, DiagnosticCacheEntry>;
194
- waitForChange: () => DiagnosticStateWait;
195
- pullDiagnostics: (options: {
128
+ interface OpenDocumentRequestCollectionOptions {
129
+ readonly requests: readonly DiagnosticSynchronization[];
130
+ readonly syncStart: number;
131
+ readonly maxWaitMs: number;
132
+ readonly signal?: AbortSignal;
133
+ readonly deadline?: number;
134
+ readonly operationId?: string;
135
+ readonly pullDiagnostics: (options: {
196
136
  request: DiagnosticSynchronization;
197
137
  timeoutMs: number;
198
138
  signal: AbortSignal;
199
139
  operationId?: string;
200
140
  deadline?: number;
201
141
  }) => Promise<boolean>;
202
- }): Promise<void> {
203
- const deadline = options.syncStart + options.maxWaitMs;
204
- const results = await Promise.allSettled(
205
- options.requests.map(async (request) => {
206
- const remaining = deadline - Date.now();
207
- if (remaining <= 0) throw new Error("pull diagnostic timeout");
208
- const pullController = new AbortController();
209
- // Link the caller's cancellation to this document's pull controller so
210
- // an in-flight pull receives protocol cancellation and stops promptly.
211
- const onAbort = () => pullController.abort(options.signal?.reason);
212
- if (options.signal?.aborted) onAbort();
213
- else options.signal?.addEventListener("abort", onAbort, { once: true });
214
- try {
215
- const outcome = await raceDiagnosticPull({
216
- pull: options.pullDiagnostics({
217
- request,
218
- timeoutMs: remaining,
219
- signal: pullController.signal,
220
- operationId: options.operationId,
221
- deadline: options.deadline,
222
- }),
223
- waitForChange: options.waitForChange,
224
- freshPush: () =>
225
- hasFreshPush(options.diagnosticStore, request, options.currentEvidenceRevision()),
226
- current: () => isCurrentSynchronization(options.openDocuments, request),
227
- });
228
- if (outcome !== "pull") pullController.abort();
229
- return outcome === "pull";
230
- } finally {
231
- options.signal?.removeEventListener("abort", onAbort);
232
- }
233
- }),
142
+ }
143
+
144
+ /** Collect request-based evidence sequentially within one refresh budget. */
145
+ export async function pullDiagnosticsForOpenDocuments(
146
+ options: OpenDocumentRequestCollectionOptions,
147
+ ): Promise<{
148
+ failedUris: readonly string[];
149
+ incompleteUris: readonly string[];
150
+ timedOut: boolean;
151
+ }> {
152
+ const deadline = Math.min(
153
+ options.syncStart + options.maxWaitMs,
154
+ options.deadline ?? Number.POSITIVE_INFINITY,
234
155
  );
156
+ const failedUris: string[] = [];
157
+ const incompleteUris: string[] = [];
158
+ let timedOut = false;
159
+
160
+ for (const request of options.requests) {
161
+ throwIfCodeRequestInterrupted({ signal: options.signal, deadline: options.deadline });
162
+ const remaining = deadline - Date.now();
163
+ if (remaining <= 0) {
164
+ timedOut = true;
165
+ break;
166
+ }
167
+ const outcome = await collectOneRefreshRequest(options, request, remaining, deadline);
168
+ if (outcome === "failed") failedUris.push(request.uri);
169
+ if (outcome === "incomplete") incompleteUris.push(request.uri);
170
+ if (outcome === "timed-out") {
171
+ timedOut = true;
172
+ break;
173
+ }
174
+ }
235
175
 
236
- // A cancelled refresh must not degrade into failed coverage evidence:
237
- // the caller no longer awaits a result.
238
176
  throwIfCodeRequestInterrupted({ signal: options.signal, deadline: options.deadline });
177
+ return { failedUris, incompleteUris, timedOut };
178
+ }
179
+
180
+ type RefreshRequestOutcome = "completed" | "failed" | "incomplete" | "timed-out";
239
181
 
240
- const incomplete = results.some((result) => result.status === "rejected" || !result.value);
241
- if (incomplete && options.requests.length > 0) {
242
- const failedUris = options.requests.flatMap((request, index) => {
243
- const result = results[index];
244
- return result?.status === "rejected" && !isDiagnosticTimeout(result.reason)
245
- ? [request.uri]
246
- : [];
182
+ async function collectOneRefreshRequest(
183
+ options: OpenDocumentRequestCollectionOptions,
184
+ request: DiagnosticSynchronization,
185
+ timeoutMs: number,
186
+ collectionDeadline: number,
187
+ ): Promise<RefreshRequestOutcome> {
188
+ const pullController = new AbortController();
189
+ const onAbort = () => pullController.abort(options.signal?.reason);
190
+ if (options.signal?.aborted) onAbort();
191
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
192
+ try {
193
+ const result = options.pullDiagnostics({
194
+ request,
195
+ timeoutMs,
196
+ signal: pullController.signal,
197
+ operationId: options.operationId,
198
+ // The adapter receives the per-refresh deadline. This prevents queued
199
+ // work from starting after the bounded refresh budget expires.
200
+ deadline: collectionDeadline,
247
201
  });
248
- throw new DiagnosticPullError(
249
- results.some((result) => result.status === "rejected" && isDiagnosticTimeout(result.reason)),
250
- failedUris,
251
- );
202
+ const completed = await raceRequestControl(result, {
203
+ signal: options.signal,
204
+ deadline: collectionDeadline,
205
+ });
206
+ return completed ? "completed" : "incomplete";
207
+ } catch (error) {
208
+ if (isDiagnosticRequestInvalidated(error)) return "incomplete";
209
+ if (
210
+ options.signal?.aborted ||
211
+ (options.deadline !== undefined && Date.now() >= options.deadline)
212
+ ) {
213
+ throw error;
214
+ }
215
+ if (isCodeRequestDeadlineError(error)) return "timed-out";
216
+ return isDiagnosticTimeout(error) ? "timed-out" : "failed";
217
+ } finally {
218
+ options.signal?.removeEventListener("abort", onAbort);
252
219
  }
253
220
  }
254
221
 
@@ -258,16 +225,13 @@ export async function pullDiagnosticsForOpenDocuments(options: {
258
225
  * A document whose disk content still matches its open fingerprint stays in
259
226
  * the server's current state:
260
227
  * - with current evidence it is reusable without document synchronization;
261
- * - without current evidence it is retained: it keeps its synchronization and
262
- * collection waits for the server's existing pipeline instead of forcing a
263
- * no-op didChange. Large push-only servers (typescript-language-server)
264
- * skip empty-to-empty publishes, so a no-op didChange of a clean file can
265
- * never confirm — it only invalidates in-progress evidence and restarts
266
- * full-program checks (#344).
228
+ * - without current evidence it is retained: it keeps its server version and
229
+ * receives fresh request or push evidence without a no-op didChange.
267
230
  *
268
- * Documents outside the current evidence revision were invalidated by a
269
- * workspace change and must resynchronize: their stale revision cannot
270
- * produce fresh evidence without an explicit sync (ADR 0020).
231
+ * An evidence invalidation applies to every open document, but only changed
232
+ * text is synchronized. This separates evidence generation
233
+ * from document synchronization and avoids restarting a server's full-program
234
+ * check for unchanged files.
271
235
  */
272
236
  function classifyReusableDocuments(options: {
273
237
  openDocuments: ReadonlyMap<string, OpenDocumentState>;
@@ -292,11 +256,6 @@ function classifyReusableDocuments(options: {
292
256
  // The resynchronization path classifies removed and unreadable files.
293
257
  continue;
294
258
  }
295
- if (document.evidenceRevision !== options.evidenceRevision) {
296
- // Invalidated generation: the resync didChange re-establishes proof.
297
- preloadedContent.set(uri, content);
298
- continue;
299
- }
300
259
  if (fingerprintDocumentContent(content) !== document.contentFingerprint) {
301
260
  preloadedContent.set(uri, content);
302
261
  continue;
@@ -327,12 +286,22 @@ interface ClientDiagnosticRefreshOptions {
327
286
  readonly failedFiles: () => ReadonlySet<string>;
328
287
  readonly isRelatedUriTracked: (uri: string) => boolean;
329
288
  readonly nextSynchronizationId: () => number;
289
+ /** Invalidate route evidence before applying a disk content change. */
290
+ readonly noteInputContentChange: () => number;
291
+ /** Keep the shared semantic barrier's verified disk observation current. */
292
+ readonly observeDiskContent: (uri: string, content: string) => void;
330
293
  readonly clearFile: (uri: string) => void;
331
294
  readonly invalidateEvidence: (uri: string) => void;
332
295
  readonly markUnversionedSyncMoment: (uri: string) => void;
333
296
  readonly clearFailedFile: (uri: string) => void;
334
- /** Server-requested refreshes bypass normal push-only evidence reuse. */
335
- readonly forceResynchronize?: boolean;
297
+ /** Shared request engine used for every request-based diagnostic source. */
298
+ readonly requestDiagnostics: (options: {
299
+ request: DiagnosticSynchronization;
300
+ timeoutMs: number;
301
+ signal?: AbortSignal;
302
+ deadline?: number;
303
+ operationId?: string;
304
+ }) => Promise<boolean>;
336
305
  readonly options: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl;
337
306
  /** Push-publication telemetry surface for this client. */
338
307
  readonly publications: {
@@ -354,20 +323,15 @@ interface PreparedRefreshDocuments {
354
323
  /** Classify reusable documents, then resynchronize only the remaining set. */
355
324
  function prepareRefreshDocuments(
356
325
  options: ClientDiagnosticRefreshOptions,
357
- supportsPull: boolean,
358
326
  evidenceRevision: number,
359
327
  ): PreparedRefreshDocuments {
360
- const reuseEnabled = !options.forceResynchronize;
361
- const classification = reuseEnabled
362
- ? classifyReusableDocuments({
363
- openDocuments: options.openDocuments,
364
- diagnosticStore: options.diagnosticStore,
365
- evidenceRevision,
366
- failedFiles: options.failedFiles(),
367
- })
368
- : undefined;
369
- const reusableUris = classification?.reusableUris ?? new Set<string>();
370
- const retainedUris = classification?.retainedUris ?? new Set<string>();
328
+ const classification = classifyReusableDocuments({
329
+ openDocuments: options.openDocuments,
330
+ diagnosticStore: options.diagnosticStore,
331
+ evidenceRevision,
332
+ failedFiles: options.failedFiles(),
333
+ });
334
+ const { reusableUris, retainedUris } = classification;
371
335
  const documentsToResynchronize = new Map(
372
336
  Array.from(options.openDocuments).filter(
373
337
  ([uri]) => !reusableUris.has(uri) && !retainedUris.has(uri),
@@ -379,10 +343,12 @@ function prepareRefreshDocuments(
379
343
  nextVersion: (uri) => nextDocumentVersion(options.versionHistory, uri),
380
344
  nextSynchronizationId: options.nextSynchronizationId,
381
345
  evidenceRevision,
346
+ noteInputContentChange: options.noteInputContentChange,
347
+ observeDiskContent: options.observeDiskContent,
382
348
  incrementalSync: options.host.usesIncrementalDocumentSync(),
383
349
  sendNotification: (method, params) => options.host.sendNotification(method, params),
384
350
  uriToFile,
385
- preloadedContent: classification?.preloadedContent,
351
+ preloadedContent: classification.preloadedContent,
386
352
  clearFile: options.clearFile,
387
353
  invalidateEvidence: options.invalidateEvidence,
388
354
  markUnversionedSyncMoment: options.markUnversionedSyncMoment,
@@ -407,7 +373,7 @@ function prepareRefreshDocuments(
407
373
  retainedSynchronizations.push({
408
374
  uri,
409
375
  synchronizationId: document.synchronizationId,
410
- evidenceRevision: document.evidenceRevision,
376
+ ...(document.evidenceRevision === evidenceRevision ? { evidenceRevision } : {}),
411
377
  });
412
378
  }
413
379
  const synchronizations = [
@@ -415,13 +381,8 @@ function prepareRefreshDocuments(
415
381
  ...retainedSynchronizations,
416
382
  ...resynchronization.synchronizations,
417
383
  ];
418
- // Pull-capable routes still request current diagnostics even when every
419
- // document synchronization is reusable.
420
384
  const fullyReusable =
421
- reuseEnabled &&
422
- !supportsPull &&
423
- options.openDocuments.size > 0 &&
424
- reusableUris.size === options.openDocuments.size;
385
+ options.openDocuments.size > 0 && reusableUris.size === options.openDocuments.size;
425
386
  return { resynchronization, synchronizations, fullyReusable };
426
387
  }
427
388
 
@@ -433,11 +394,19 @@ export async function refreshClientOpenDiagnostics(
433
394
  // resynchronization or protocol traffic may start for a pass the caller
434
395
  // no longer awaits.
435
396
  throwIfCodeRequestInterrupted(options.options);
436
- const supportsPull = options.host.supportsPullDiagnostics();
437
- const observer = new DiagnosticObserver("refresh-open", supportsPull, options.options, {
438
- server: options.host.server,
439
- cwd: options.host.cwd,
440
- });
397
+ const requestAdapter = options.host.diagnosticRequestAdapter;
398
+ const hasDiagnosticRequestAdapter = Array.from(options.openDocuments.keys()).some((uri) =>
399
+ requestAdapter.supports(uri),
400
+ );
401
+ const observer = new DiagnosticObserver(
402
+ "refresh-open",
403
+ hasDiagnosticRequestAdapter,
404
+ options.options,
405
+ {
406
+ server: options.host.server,
407
+ cwd: options.host.cwd,
408
+ },
409
+ );
441
410
  if (!options.host.isOperational()) {
442
411
  observer.skipped(options.requestedFiles.length);
443
412
  return summarizeDiagnosticEvidence(
@@ -452,11 +421,11 @@ export async function refreshClientOpenDiagnostics(
452
421
  const maxWaitMs = options.options.maxWaitMs ?? 3_000;
453
422
  const quietMs = options.options.quietMs ?? 200;
454
423
  const syncStart = Date.now();
455
- const prepared = prepareRefreshDocuments(options, supportsPull, options.evidenceRevision());
424
+ const prepared = prepareRefreshDocuments(options, options.evidenceRevision());
456
425
  const resynchronization = prepared.resynchronization;
457
- let synchronizations = prepared.synchronizations;
426
+ const synchronizations = prepared.synchronizations;
458
427
  let failedPullUris: ReadonlySet<string> = new Set();
459
- const buildEvidence = (pushQuietMs?: number) =>
428
+ const buildEvidence = () =>
460
429
  buildDiagnosticRefreshEvidence({
461
430
  requestedFiles: options.requestedFiles,
462
431
  resynchronization,
@@ -467,16 +436,8 @@ export async function refreshClientOpenDiagnostics(
467
436
  currentEvidenceRevision: options.evidenceRevision(),
468
437
  openDocuments: options.openDocuments,
469
438
  diagnosticStore: options.diagnosticStore,
470
- pushQuietMs,
471
439
  });
472
440
 
473
- // A fully reusable push-only refresh has no protocol work to collect.
474
- // Preserve the existing cache timing event format and return current evidence.
475
- if (prepared.fullyReusable) {
476
- observer.cacheReused(synchronizations.length);
477
- return buildEvidence();
478
- }
479
-
480
441
  const settleEpoch = options.waiters.settleEpoch;
481
442
  observer.synchronized();
482
443
  if (synchronizations.length === 0) {
@@ -484,63 +445,84 @@ export async function refreshClientOpenDiagnostics(
484
445
  return buildEvidence();
485
446
  }
486
447
 
487
- if (supportsPull) {
488
- const pull = await collectPullEvidenceForRefresh({
489
- options,
490
- synchronizations,
491
- syncStart,
492
- maxWaitMs,
493
- observer,
494
- });
495
- failedPullUris = new Set(pull.failedPullUris);
496
- if (pull.completed) return buildEvidence();
448
+ const requestSynchronizations = synchronizations.filter((synchronization) =>
449
+ requestAdapter.supports(synchronization.uri),
450
+ );
451
+ // A fully reusable push-only refresh has no protocol work to collect.
452
+ if (prepared.fullyReusable && requestSynchronizations.length === 0) {
453
+ observer.cacheReused(synchronizations.length);
454
+ return buildEvidence();
497
455
  }
456
+ const pushSynchronizations = synchronizations.filter(
457
+ (synchronization) => !requestSynchronizations.includes(synchronization),
458
+ );
498
459
 
499
- const waitForDiagnosticSettle = (settleStart: number, settleGeneration: number) =>
460
+ const waitForDiagnosticSettle = (
461
+ targetSynchronizations: readonly DiagnosticSynchronization[],
462
+ settleStart: number,
463
+ settleGeneration: number,
464
+ ) =>
500
465
  options.waiters.waitForSettle(
501
466
  {
502
467
  syncStart: settleStart,
503
468
  maxWaitMs,
504
469
  quietMs,
505
470
  settleEpoch: settleGeneration,
506
- isComplete: () =>
507
- synchronizations.every((item) =>
508
- hasFreshEvidence(options.diagnosticStore, item, options.evidenceRevision()),
509
- ),
510
471
  latestReceived: () =>
511
472
  latestCurrentEvidenceReceivedAt(
512
473
  options.diagnosticStore,
513
- synchronizations,
474
+ targetSynchronizations,
514
475
  options.evidenceRevision(),
515
476
  ),
516
477
  },
517
478
  options.options,
518
479
  );
519
480
 
520
- let finalSettle = await waitForDiagnosticSettle(syncStart, settleEpoch);
521
- if (!supportsPull && finalSettle.outcome === "timed-out") {
522
- const reopen = await reopenUnconfirmedDocuments({
481
+ if (requestSynchronizations.length > 0) {
482
+ const request = await collectPullEvidenceForRefresh({
523
483
  options,
524
- synchronizations,
525
- reopenCandidates: resynchronization.resynchronizedUris,
526
- observer,
484
+ synchronizations: requestSynchronizations,
485
+ syncStart,
486
+ maxWaitMs,
527
487
  });
528
- if (reopen.performed) {
529
- synchronizations = reopen.synchronizations;
530
- // A large push-only project may still be processing the reopen batch.
531
- // The replacement pass uses the same collection budget as the initial pass.
532
- finalSettle = await waitForDiagnosticSettle(reopen.startedAt, options.waiters.settleEpoch);
488
+ failedPullUris = new Set(request.failedPullUris);
489
+ const source = requestSourceFor(options.host, requestSynchronizations);
490
+ if (pushSynchronizations.length === 0) {
491
+ if (request.completed) observer.requestCompleted(source, requestSynchronizations.length);
492
+ else observer.requestIncomplete(source, request.timedOut, requestSynchronizations.length);
493
+ throwIfCodeRequestInterrupted(options.options);
494
+ emitRefreshPublicationSummary(options, synchronizations);
495
+ return buildEvidence();
496
+ }
497
+ if (request.completed) {
498
+ observer.requestCompleted(source, requestSynchronizations.length, false);
499
+ } else {
500
+ observer.requestIncomplete(source, request.timedOut, requestSynchronizations.length, false);
533
501
  }
502
+ const finalSettle = await waitForDiagnosticSettle(pushSynchronizations, syncStart, settleEpoch);
503
+ observer.mixedSettled(source, synchronizations.length, finalSettle);
504
+ // Request failures are final for this bounded pass. Ambient pushes can
505
+ // remain useful observations, but they cannot confirm the request route.
506
+ throwIfCodeRequestInterrupted(options.options);
507
+ emitRefreshPublicationSummary(options, synchronizations);
508
+ return buildEvidence();
534
509
  }
510
+
511
+ const finalSettle = await waitForDiagnosticSettle(synchronizations, syncStart, settleEpoch);
535
512
  observer.pushSettled(synchronizations.length, finalSettle);
536
513
  // A cancelled settle must not publish evidence the caller no longer awaits.
537
514
  throwIfCodeRequestInterrupted(options.options);
515
+ emitRefreshPublicationSummary(options, synchronizations);
516
+ return buildEvidence();
517
+ }
518
+
519
+ function emitRefreshPublicationSummary(
520
+ options: ClientDiagnosticRefreshOptions,
521
+ synchronizations: readonly DiagnosticSynchronization[],
522
+ ): void {
538
523
  options.publications.emitSummary({
539
524
  operation: "refresh-open",
540
- identity: {
541
- server: options.host.server,
542
- cwd: options.host.cwd,
543
- },
525
+ identity: { server: options.host.server, cwd: options.host.cwd },
544
526
  synchronizations: synchronizations.map((synchronization) => ({
545
527
  uri: synchronization.uri,
546
528
  synchronizationId: synchronization.synchronizationId,
@@ -553,139 +535,56 @@ export async function refreshClientOpenDiagnostics(
553
535
  })),
554
536
  operationId: options.options.operationId,
555
537
  });
556
- return buildEvidence(quietMs);
557
538
  }
558
539
 
559
- /**
560
- * Pull diagnostic evidence for every synchronized document, or fall through
561
- * to the push settle path when any pull fails. An interruption during the
562
- * pull phase stops the refresh instead of degrading into failed coverage.
563
- */
540
+ /** Collect request evidence for every applicable synchronized document. */
564
541
  async function collectPullEvidenceForRefresh(options: {
565
542
  options: ClientDiagnosticRefreshOptions;
566
543
  synchronizations: readonly DiagnosticSynchronization[];
567
544
  syncStart: number;
568
545
  maxWaitMs: number;
569
- observer: DiagnosticObserver;
570
- }): Promise<{ completed: boolean; failedPullUris: ReadonlySet<string> }> {
571
- const { options: refresh, synchronizations, syncStart, maxWaitMs, observer } = options;
546
+ }): Promise<{ completed: boolean; failedPullUris: ReadonlySet<string>; timedOut: boolean }> {
547
+ const { options: refresh, synchronizations, syncStart, maxWaitMs } = options;
572
548
  try {
573
- await pullDiagnosticsForOpenDocuments({
549
+ const result = await pullDiagnosticsForOpenDocuments({
574
550
  requests: synchronizations,
575
551
  syncStart,
576
552
  maxWaitMs,
577
553
  signal: refresh.options.signal,
578
554
  deadline: refresh.options.deadline,
579
555
  operationId: refresh.options.operationId,
580
- currentEvidenceRevision: refresh.evidenceRevision,
581
- openDocuments: refresh.openDocuments,
582
- diagnosticStore: refresh.diagnosticStore,
583
- waitForChange: () => refresh.waiters.waitForChange(),
584
556
  pullDiagnostics: (pullOptions) =>
585
- pullClientDiagnosticEvidenceFromHost({
586
- host: refresh.host,
587
- store: refresh.diagnosticStore,
588
- openDocuments: refresh.openDocuments,
589
- currentEvidenceRevision: refresh.evidenceRevision,
590
- isRelatedUriTracked: refresh.isRelatedUriTracked,
591
- request: {
592
- uri: pullOptions.request.uri,
593
- timeoutMs: pullOptions.timeoutMs,
594
- synchronizationId: pullOptions.request.synchronizationId,
595
- evidenceRevision:
596
- refresh.openDocuments.get(pullOptions.request.uri)?.evidenceRevision ??
597
- refresh.evidenceRevision(),
598
- signal: pullOptions.signal,
599
- deadline: pullOptions.deadline,
600
- operationId: pullOptions.operationId,
601
- },
557
+ refresh.requestDiagnostics({
558
+ request: pullOptions.request,
559
+ timeoutMs: pullOptions.timeoutMs,
560
+ signal: pullOptions.signal,
561
+ deadline: pullOptions.deadline,
562
+ operationId: pullOptions.operationId,
602
563
  }),
603
564
  });
604
- observer.pullCompleted(synchronizations.length);
605
- return { completed: true, failedPullUris: new Set() };
565
+ const failedPullUris = new Set(result.failedUris);
566
+ const incomplete =
567
+ result.incompleteUris.length > 0 || failedPullUris.size > 0 || result.timedOut;
568
+ return {
569
+ completed: !incomplete,
570
+ failedPullUris,
571
+ timedOut: result.timedOut,
572
+ };
606
573
  } catch (error) {
607
- observer.pullFailed(error);
608
- if (error instanceof DiagnosticPullError) {
609
- return { completed: false, failedPullUris: new Set(error.failedUris) };
610
- }
611
574
  if (isCodeRequestInterruption(error, refresh.options)) throw error;
612
- return { completed: false, failedPullUris: new Set() };
575
+ return { completed: false, failedPullUris: new Set(), timedOut: false };
613
576
  }
614
577
  }
615
578
 
616
- /**
617
- * Reopen-resync fallback (R2): on push-only routes a document that was
618
- * didChange-synchronized and stays unconfirmed after the settle window may
619
- * have been skipped by the server — a clean file gets no push on didChange
620
- * at all, but the server publishes on didOpen. Close and reopen each such
621
- * document so the server publishes, then settle again within a bounded
622
- * second window. The cache entry and version history survive the reopen;
623
- * other documents keep their server state.
624
- *
625
- * Only documents this pass resynchronized are candidates: retained documents
626
- * wait for the server's existing pipeline, and reopening them would cancel
627
- * in-progress server work without fixing any publish gap (#344).
628
- */
629
- async function reopenUnconfirmedDocuments(options: {
630
- options: ClientDiagnosticRefreshOptions;
631
- synchronizations: readonly DiagnosticSynchronization[];
632
- /** URIs that received a didChange in this pass and may need a reopen push. */
633
- reopenCandidates: ReadonlySet<string>;
634
- observer: DiagnosticObserver;
635
- }): Promise<{
636
- performed: boolean;
637
- startedAt: number;
638
- synchronizations: DiagnosticSynchronization[];
639
- }> {
640
- const { options: refresh, synchronizations, reopenCandidates, observer } = options;
641
- const startedAt = Date.now();
642
- const unconfirmed = synchronizations.filter(
643
- (item) =>
644
- reopenCandidates.has(item.uri) &&
645
- // A document with any current publication — tentative included — is
646
- // not a reopen candidate: its server pipeline is alive and a republish
647
- // can still promote the retained cache (ADR 0021).
648
- !hasCurrentEvidence(refresh.diagnosticStore, item, refresh.evidenceRevision()),
579
+ function requestSourceFor(
580
+ host: ClientDiagnosticsHost,
581
+ synchronizations: readonly DiagnosticSynchronization[],
582
+ ): "pull" | "typescript" | "mixed" {
583
+ const sources = new Set(
584
+ synchronizations.map(
585
+ (synchronization) => host.diagnosticRequestAdapter.sourceFor(synchronization.uri) ?? "pull",
586
+ ),
649
587
  );
650
- const reopenedSynchronizations: DiagnosticSynchronization[] = [];
651
- for (const item of unconfirmed) {
652
- const document = refresh.openDocuments.get(item.uri);
653
- if (!document) continue;
654
- const filePath = uriToFile(item.uri);
655
- let content: string;
656
- try {
657
- content = readFileSync(filePath, "utf-8");
658
- } catch {
659
- // The file disappeared mid-refresh; keep the document as-is and
660
- // report its current unconfirmed coverage.
661
- continue;
662
- }
663
- reopenDocument({
664
- uri: item.uri,
665
- content,
666
- document,
667
- languageId: detectLanguageId(filePath),
668
- nextVersion: () => nextDocumentVersion(refresh.versionHistory, item.uri),
669
- nextSynchronizationId: refresh.nextSynchronizationId,
670
- evidenceRevision: refresh.evidenceRevision(),
671
- waiters: refresh.waiters,
672
- sendNotification: (method, params) => refresh.host.sendNotification(method, params),
673
- markUnversionedSyncMoment: () => refresh.markUnversionedSyncMoment(item.uri),
674
- });
675
- reopenedSynchronizations.push({
676
- uri: item.uri,
677
- synchronizationId: document.synchronizationId,
678
- evidenceRevision: document.evidenceRevision,
679
- });
680
- }
681
- if (reopenedSynchronizations.length === 0) {
682
- return { performed: false, startedAt, synchronizations: [...synchronizations] };
683
- }
684
- observer.reopened(reopenedSynchronizations.length);
685
- const reopenedByUri = new Map(reopenedSynchronizations.map((item) => [item.uri, item]));
686
- return {
687
- performed: true,
688
- startedAt,
689
- synchronizations: synchronizations.map((item) => reopenedByUri.get(item.uri) ?? item),
690
- };
588
+ if (sources.size === 1) return sources.values().next().value as "pull" | "typescript";
589
+ return "mixed";
691
590
  }