@mrclrchtr/supi-code-intelligence 4.10.0 → 5.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 (203) hide show
  1. package/README.md +2 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  6. package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  8. package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  9. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  13. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
  28. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +71 -11
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +365 -297
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
  44. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
  45. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
  47. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
  50. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
  52. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
  54. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
  56. package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
  57. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
  60. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
  61. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
  62. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
  63. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
  64. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +17 -7
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +6 -3
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
  122. package/package.json +5 -5
  123. package/src/analysis/health/diagnostics.ts +236 -24
  124. package/src/analysis/health/recovery.ts +6 -2
  125. package/src/analysis/provider.ts +58 -0
  126. package/src/analysis/readiness.ts +30 -9
  127. package/src/analysis/refactor/apply.ts +45 -1
  128. package/src/analysis/refactor/mutation-authority.ts +143 -0
  129. package/src/analysis/relations/callees.ts +9 -1
  130. package/src/analysis/relations/types.ts +5 -2
  131. package/src/analysis/search/ast-scan-timing.ts +9 -2
  132. package/src/analysis/search/ast-scan.ts +1 -1
  133. package/src/analysis/search/deadline.ts +2 -2
  134. package/src/analysis/search/pattern-analysis.ts +46 -32
  135. package/src/analysis/search/pattern.ts +12 -2
  136. package/src/analysis/target/anchored.ts +20 -11
  137. package/src/analysis/target/file.ts +18 -11
  138. package/src/analysis/target/identity.ts +19 -3
  139. package/src/analysis/target/symbol.ts +36 -15
  140. package/src/api.ts +4 -0
  141. package/src/diagnostics/evidence.ts +51 -0
  142. package/src/session/capability-adapter.ts +12 -3
  143. package/src/session/find-workflow.ts +17 -7
  144. package/src/session/graph/collect.ts +18 -2
  145. package/src/session/graph-workflow.ts +19 -12
  146. package/src/session/health-refresh.ts +148 -0
  147. package/src/session/health-types.ts +37 -15
  148. package/src/session/health-workflow.ts +37 -39
  149. package/src/session/inspect/collect.ts +20 -3
  150. package/src/session/inspect-workflow.ts +10 -5
  151. package/src/session/orientation/collect.ts +10 -1
  152. package/src/session/orientation/context-facts.ts +3 -1
  153. package/src/session/orientation/context-sections.ts +20 -7
  154. package/src/session/orientation/gather.ts +17 -11
  155. package/src/session/orientation-types.ts +2 -1
  156. package/src/session/orientation-workflow.ts +15 -8
  157. package/src/session/refactor-plans.ts +2 -0
  158. package/src/session/refactor-workflow.ts +59 -14
  159. package/src/session/session.ts +4 -1
  160. package/src/session/target-workflow.ts +54 -23
  161. package/src/session/workflow-control.ts +7 -3
  162. package/src/substrate/lsp/lifecycle.ts +26 -46
  163. package/src/substrate/lsp/maintenance.ts +199 -25
  164. package/src/substrate/lsp/state.ts +2 -2
  165. package/src/substrate/workspace-provider-host.ts +25 -3
  166. package/src/tool/find/execute.ts +7 -1
  167. package/src/tool/find/render.ts +20 -3
  168. package/src/tool/find/tui.ts +23 -21
  169. package/src/tool/graph/execute.ts +23 -3
  170. package/src/tool/graph/tui.ts +59 -46
  171. package/src/tool/health/execute.ts +7 -2
  172. package/src/tool/health/markdown.ts +56 -14
  173. package/src/tool/health/refresh-status.ts +105 -0
  174. package/src/tool/health/tui.ts +95 -98
  175. package/src/tool/infra/workflow-control.ts +8 -0
  176. package/src/tool/inspect/execute.ts +7 -1
  177. package/src/tool/inspect/tui.ts +26 -18
  178. package/src/tool/orientation/execute.ts +21 -3
  179. package/src/tool/orientation/tui.ts +51 -35
  180. package/src/tool/refactor-apply/execute.ts +3 -0
  181. package/src/tool/refactor-apply/tui.ts +43 -25
  182. package/src/tool/refactor-plan/execute.ts +4 -0
  183. package/src/tool/refactor-plan/tui.ts +33 -20
  184. package/src/tool/register.ts +127 -22
  185. package/src/tool/resolve/execute.ts +18 -1
  186. package/src/tool/resolve/tui.ts +42 -24
  187. package/src/tool/result/display.ts +107 -0
  188. package/src/tool/result/errors.ts +20 -17
  189. package/src/tool/result/find.ts +52 -8
  190. package/src/tool/result/graph.ts +44 -1
  191. package/src/tool/result/health.ts +53 -1
  192. package/src/tool/result/inspect.ts +113 -1
  193. package/src/tool/result/orientation.ts +66 -14
  194. package/src/tool/result/refactor.ts +36 -4
  195. package/src/tool/result/resolve.ts +44 -7
  196. package/src/tool/result/types.ts +20 -0
  197. package/src/types/execution.ts +18 -5
  198. package/src/types/index.ts +8 -1
  199. package/src/ui/status-command.ts +5 -3
  200. package/src/ui/status-overlay.ts +4 -0
  201. package/src/ui/tui/common.ts +108 -10
  202. package/src/ui/tui/display.ts +48 -0
  203. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -351
@@ -1,62 +1,63 @@
1
- // LSP client document synchronization and diagnostic state.
2
-
3
- import { existsSync, readFileSync } from "node:fs";
4
- import type {
5
- Diagnostic,
6
- DocumentDiagnosticReport,
7
- PublishDiagnosticsParams,
8
- TextDocumentIdentifier,
9
- TextDocumentItem,
10
- VersionedTextDocumentIdentifier,
11
- } from "../config/types.ts";
1
+ // biome-ignore-all lint/style/noExcessiveLinesPerFile: one client's document sync, diagnostics, refresh, and sync-file flow stay in one cohesive class.
2
+ import * as path from "node:path";
3
+ import {
4
+ type CodeQueryResult,
5
+ type CodeRequestControl,
6
+ completedCodeQuery,
7
+ throwIfCodeRequestInterrupted,
8
+ unavailableCodeQuery,
9
+ } from "@mrclrchtr/supi-code-runtime/api";
10
+ import type { Diagnostic, TextDocumentItem } from "../config/types.ts";
11
+ import type { DiagnosticEvidenceSummary } from "../diagnostics/evidence.ts";
12
12
  import { detectLanguageId, fileToUri, uriToFile } from "../utils.ts";
13
+ import { applyPushDiagnostics, buildClientDiagnosticSnapshot } from "./client-diagnostic-cache.ts";
14
+ import { collectSynchronizedFileDiagnostics } from "./client-diagnostic-collection.ts";
15
+ import {
16
+ type DiagnosticCacheEntry,
17
+ type DiagnosticSynchronization,
18
+ hasFreshPush,
19
+ isCurrentSynchronization,
20
+ isValidPublishDiagnosticsParams,
21
+ nextDocumentVersion,
22
+ } from "./client-diagnostic-evidence.ts";
23
+ import type { ClientDiagnosticsHost } from "./client-diagnostic-host.ts";
24
+ import {
25
+ pullClientDiagnosticEvidenceFromHost,
26
+ refreshClientOpenDiagnostics,
27
+ sendDidCloseNotification,
28
+ } from "./client-diagnostic-refresh.ts";
29
+ import { DiagnosticObserver, type DiagnosticPushWaitOutcome } from "./client-diagnostic-timing.ts";
30
+ import { DiagnosticWaitRegistry } from "./client-diagnostic-waiters.ts";
13
31
  import {
14
- DiagnosticObserver,
15
- DiagnosticPullError,
16
- type DiagnosticPushWaitOutcome,
17
- type DiagnosticSettleResult,
18
- isDiagnosticTimeout,
19
- } from "./client-diagnostic-timing.ts";
32
+ type ClientDiagnosticSnapshot,
33
+ type DiagnosticEntry,
34
+ fingerprintDocumentContent,
35
+ hasCurrentDiagnosticEvidence,
36
+ type OpenDocumentState,
37
+ } from "./client-document-state.ts";
38
+ import {
39
+ clearTrackedDocumentState,
40
+ reopenDocument,
41
+ synchronizeTrackedDocument,
42
+ } from "./client-document-sync.ts";
43
+ import { getDiagnosticFileState } from "./client-file-state.ts";
20
44
 
21
45
  const DIAGNOSTIC_WAIT_MS = 3_000;
22
-
23
- type OpenDocument = { version: number };
24
- type DiagnosticWaiter = (outcome: "published" | "released") => void;
25
-
26
- type DiagnosticCacheEntry = {
27
- diagnostics: Diagnostic[];
28
- receivedAt: number;
29
- version?: number;
30
- resultId?: string;
31
- };
32
-
33
- /** One file's stored diagnostics as consumed by manager-level collection. */
34
- export interface DiagnosticEntry {
35
- uri: string;
36
- diagnostics: Diagnostic[];
37
- }
38
-
39
- interface ClientDiagnosticsHost {
40
- isOperational(): boolean;
41
- supportsPullDiagnostics(): boolean;
42
- sendNotification(method: string, params: unknown): void;
43
- pullDocumentDiagnostics(
44
- uri: string,
45
- previousResultId: string | undefined,
46
- timeoutMs: number,
47
- ): Promise<DocumentDiagnosticReport | null>;
48
- }
49
-
50
- /**
51
- * Owns one LSP client's open documents, diagnostic cache, waiters, and refresh policy.
52
- *
53
- * The host supplies transport and readiness operations. Callers use `LspClient` as
54
- * the external seam and do not access this state directly.
55
- */
46
+ /** Bounded push wait after a reopen-resync fallback, in milliseconds. */
47
+ const REOPEN_EVIDENCE_WAIT_MS = 1_000;
48
+ /** Own one client's document and diagnostic evidence; revisions prevent stale reuse. */
56
49
  export class ClientDiagnostics {
57
- readonly #openDocs = new Map<string, OpenDocument>();
50
+ readonly #openDocs = new Map<string, OpenDocumentState>();
58
51
  readonly #diagnosticStore = new Map<string, DiagnosticCacheEntry>();
59
- readonly #diagnosticWaiters = new Map<string, DiagnosticWaiter[]>();
52
+ readonly #waiters = new DiagnosticWaitRegistry();
53
+ readonly #versionHistory = new Map<string, number>();
54
+ readonly #failedUris = new Set<string>();
55
+ /** Client-side sync moment per URI: unversioned pushes before it stay rejected. */
56
+ readonly #unversionedPushSyncMoments = new Map<string, number>();
57
+ /** URIs closed by a lifecycle operation: versioned pushes stay fail-closed. */
58
+ readonly #closedVersionedBarrier = new Set<string>();
59
+ #evidenceRevision = 0;
60
+ #nextSynchronizationId = 0;
60
61
 
61
62
  constructor(private readonly host: ClientDiagnosticsHost) {}
62
63
 
@@ -64,36 +65,58 @@ export class ClientDiagnostics {
64
65
  return Array.from(this.#openDocs.keys()).map(uriToFile);
65
66
  }
66
67
 
67
- /** Release all waiters and remove all document and diagnostic state. */
68
- clear(): void {
68
+ clear(options: { preserveFailedDocuments?: boolean } = {}): void {
69
+ if (options.preserveFailedDocuments) {
70
+ for (const uri of [...this.#openDocs.keys(), ...this.#diagnosticStore.keys()]) {
71
+ this.#failedUris.add(uri);
72
+ this.#closedVersionedBarrier.add(uri);
73
+ }
74
+ } else {
75
+ this.#failedUris.clear();
76
+ this.#closedVersionedBarrier.clear();
77
+ }
69
78
  this.#openDocs.clear();
70
79
  this.#diagnosticStore.clear();
71
- this.#releaseAllDiagnosticWaiters();
80
+ this.#versionHistory.clear();
81
+ this.#evidenceRevision++;
82
+ this.#unversionedPushSyncMoments.clear();
83
+ this.#waiters.releaseAll();
84
+ this.#waiters.cancelSettle();
72
85
  }
73
86
 
74
- /** Open a document, or update it when it is already open. */
75
87
  didOpen(filePath: string, content: string): void {
76
88
  if (!this.host.isOperational()) return;
77
89
 
78
90
  const uri = fileToUri(filePath);
91
+ this.#failedUris.delete(uri);
79
92
  if (this.#openDocs.has(uri)) {
80
93
  this.didChange(filePath, content);
81
94
  return;
82
95
  }
83
96
 
84
97
  const languageId = detectLanguageId(filePath);
85
- this.#openDocs.set(uri, { version: 1 });
98
+ this.#waiters.cancelSettle();
99
+ // A didOpen is a synchronization: pushes the server sends after it are
100
+ // responses to the open and can become fresh evidence (ADR 0020).
101
+ this.#unversionedPushSyncMoments.set(uri, Date.now());
102
+ this.#closedVersionedBarrier.delete(uri);
103
+ const version = nextDocumentVersion(this.#versionHistory, uri);
104
+ this.#openDocs.set(uri, {
105
+ version,
106
+ synchronizationId: ++this.#nextSynchronizationId,
107
+ evidenceRevision: this.#evidenceRevision,
108
+ contentFingerprint: fingerprintDocumentContent(content),
109
+ });
86
110
  this.host.sendNotification("textDocument/didOpen", {
87
111
  textDocument: {
88
112
  uri,
89
113
  languageId,
90
- version: 1,
114
+ version,
91
115
  text: content,
92
116
  } satisfies TextDocumentItem,
93
117
  });
94
118
  }
95
119
 
96
- /** Update a document, or open it when it is not tracked yet. */
97
120
  didChange(filePath: string, content: string): void {
98
121
  if (!this.host.isOperational()) return;
99
122
 
@@ -103,295 +126,340 @@ export class ClientDiagnostics {
103
126
  this.didOpen(filePath, content);
104
127
  return;
105
128
  }
106
-
107
- doc.version++;
108
- this.#sendDidChange(uri, doc.version, content);
129
+ const nextFingerprint = fingerprintDocumentContent(content);
130
+ if (doc.contentFingerprint === nextFingerprint) return;
131
+ synchronizeTrackedDocument({
132
+ uri,
133
+ content,
134
+ document: doc,
135
+ nextVersion: () => nextDocumentVersion(this.#versionHistory, uri),
136
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
137
+ evidenceRevision: this.#evidenceRevision,
138
+ waiters: this.#waiters,
139
+ sendNotification: (method, params) => this.host.sendNotification(method, params),
140
+ markUnversionedSyncMoment: () => this.#unversionedPushSyncMoments.set(uri, Date.now()),
141
+ clearFailedFile: () => this.#failedUris.delete(uri),
142
+ open: () => this.didOpen(filePath, content),
143
+ });
109
144
  }
110
145
 
111
- /** Close a document and remove its cached diagnostic state. */
112
146
  didClose(filePath: string): void {
113
147
  const uri = fileToUri(filePath);
114
148
  const wasOpen = this.#openDocs.has(uri);
115
- this.#clearFileState(uri);
149
+ this.#failedUris.delete(uri);
150
+ this.#unversionedPushSyncMoments.delete(uri);
151
+ this.#closedVersionedBarrier.add(uri);
152
+ clearTrackedDocumentState(this.#openDocs, this.#diagnosticStore, this.#waiters, uri);
116
153
 
117
154
  if (wasOpen && this.host.isOperational()) {
118
- this.#sendDidClose(uri);
155
+ sendDidCloseNotification(this.host, uri);
119
156
  }
120
157
  }
121
158
 
122
- /** Remove missing open documents and diagnostics, and return their file paths. */
123
159
  pruneMissingFiles(): string[] {
124
160
  const uris = new Set([...this.#openDocs.keys(), ...this.#diagnosticStore.keys()]);
125
161
  const removedFiles: string[] = [];
126
162
 
127
163
  for (const uri of uris) {
128
164
  const filePath = uriToFile(uri);
129
- if (existsSync(filePath)) continue;
165
+ if (getDiagnosticFileState(filePath) !== "removed") continue;
130
166
 
131
167
  const wasOpen = this.#openDocs.has(uri);
132
- this.#clearFileState(uri);
168
+ this.#failedUris.add(uri);
169
+ this.#unversionedPushSyncMoments.delete(uri);
170
+ this.#closedVersionedBarrier.add(uri);
171
+ clearTrackedDocumentState(this.#openDocs, this.#diagnosticStore, this.#waiters, uri);
133
172
  removedFiles.push(filePath);
134
- if (wasOpen && this.host.isOperational()) this.#sendDidClose(uri);
173
+ if (wasOpen && this.host.isOperational()) sendDidCloseNotification(this.host, uri);
135
174
  }
136
175
 
137
176
  return removedFiles;
138
177
  }
178
+ /** Retain a failed document outcome when a replacement cannot reopen it. */
179
+ markFailedFile(filePath: string): void {
180
+ const uri = fileToUri(filePath);
181
+ this.#failedUris.add(uri);
182
+ this.#unversionedPushSyncMoments.delete(uri);
183
+ this.#closedVersionedBarrier.add(uri);
184
+ }
139
185
 
186
+ getOpenDocumentVersion(filePath: string): number | null {
187
+ return this.#openDocs.get(fileToUri(filePath))?.version ?? null;
188
+ }
140
189
  getDiagnostics(filePath: string): Diagnostic[] {
141
190
  return this.#diagnosticStore.get(fileToUri(filePath))?.diagnostics ?? [];
142
191
  }
143
-
144
- /** Return non-empty diagnostics for files that still exist. */
192
+ getDiagnosticSnapshot(): ClientDiagnosticSnapshot {
193
+ return buildClientDiagnosticSnapshot({
194
+ store: this.#diagnosticStore,
195
+ openDocuments: this.#openDocs,
196
+ failedDocuments: this.#failedUris,
197
+ evidenceRevision: this.#evidenceRevision,
198
+ });
199
+ }
145
200
  getAllDiagnostics(): DiagnosticEntry[] {
146
- const result: DiagnosticEntry[] = [];
147
- for (const [uri, entry] of this.#diagnosticStore) {
148
- if (entry.diagnostics.length === 0 || !existsSync(uriToFile(uri))) continue;
149
- result.push({ uri, diagnostics: entry.diagnostics });
150
- }
151
- return result;
201
+ return this.getDiagnosticSnapshot().entries;
152
202
  }
153
-
154
- /** Force the next pull refresh to request complete diagnostic reports. */
155
203
  clearPullResultIds(): void {
156
204
  for (const entry of this.#diagnosticStore.values()) delete entry.resultId;
157
205
  }
158
-
159
- /** Apply diagnostics received through `textDocument/publishDiagnostics`. */
160
- handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
161
- if (params.version !== undefined && params.version !== null) {
162
- const openDoc = this.#openDocs.get(params.uri);
163
- if (openDoc && params.version < openDoc.version) return;
206
+ /** Invalidate cache proof while retaining its data as partial fallback. */
207
+ invalidateCachedEvidence(): void {
208
+ this.#evidenceRevision++;
209
+ const knownUris = new Set([
210
+ ...this.#openDocs.keys(),
211
+ ...this.#diagnosticStore.keys(),
212
+ ...this.#versionHistory.keys(),
213
+ ...this.#failedUris,
214
+ ]);
215
+ // A watched-file notification is sent immediately after this call, so the
216
+ // recorded moment gates unversioned pushes around the invalidation: only
217
+ // pushes the server sends after the change can become fresh evidence.
218
+ const moment = Date.now();
219
+ for (const uri of knownUris) {
220
+ this.#unversionedPushSyncMoments.set(uri, moment);
221
+ this.#closedVersionedBarrier.add(uri);
164
222
  }
165
-
166
- this.#diagnosticStore.set(params.uri, {
167
- diagnostics: params.diagnostics,
168
- receivedAt: Date.now(),
169
- version: params.version ?? undefined,
170
- });
171
- this.#releaseDiagnosticWaiters(params.uri, "published");
172
223
  }
173
224
 
174
- /** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
175
- async refreshOpenDiagnostics(
176
- options: { maxWaitMs?: number; quietMs?: number } = {},
177
- ): Promise<void> {
178
- const supportsPull = this.host.supportsPullDiagnostics();
179
- const observer = new DiagnosticObserver("refresh-open", supportsPull);
180
- if (!this.host.isOperational()) {
181
- observer.skipped(0);
225
+ handlePublishDiagnostics(params: unknown): void {
226
+ if (!isValidPublishDiagnosticsParams(params)) return;
227
+ if (
228
+ !applyPushDiagnostics({
229
+ store: this.#diagnosticStore,
230
+ openDocuments: this.#openDocs,
231
+ params,
232
+ evidenceRevision: this.#evidenceRevision,
233
+ unversionedSyncMoment: this.#unversionedPushSyncMoments.get(params.uri),
234
+ closedVersionedBarrier: this.#closedVersionedBarrier.has(params.uri),
235
+ })
236
+ ) {
182
237
  return;
183
238
  }
184
-
185
- const maxWaitMs = options.maxWaitMs ?? 3_000;
186
- const quietMs = options.quietMs ?? 200;
187
- const syncStart = Date.now();
188
- this.#resyncOpenDocuments();
189
- observer.synchronized();
190
- const documentCount = this.#openDocs.size;
191
- if (documentCount === 0) {
192
- observer.skipped(0);
193
- return;
194
- }
195
-
196
- if (supportsPull) {
197
- try {
198
- await this.#pullDiagnosticsForOpenDocuments(syncStart, maxWaitMs);
199
- observer.pullCompleted(documentCount);
200
- return;
201
- } catch (error) {
202
- observer.pullFailed(error);
203
- }
204
- }
205
-
206
- const settle = await this.#waitForDiagnosticSettle(syncStart, maxWaitMs, quietMs);
207
- observer.pushSettled(documentCount, settle);
239
+ this.#waiters.releaseFile(params.uri, "published");
240
+ this.#waiters.notifySettle();
241
+ }
242
+ async refreshOpenDiagnostics(
243
+ options: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl = {},
244
+ ): Promise<DiagnosticEvidenceSummary> {
245
+ const requestedFiles = Array.from(
246
+ new Set([...this.#openDocs.keys(), ...this.#diagnosticStore.keys(), ...this.#failedUris]),
247
+ ).map(uriToFile);
248
+ return refreshClientOpenDiagnostics({
249
+ host: this.host,
250
+ openDocuments: this.#openDocs,
251
+ diagnosticStore: this.#diagnosticStore,
252
+ waiters: this.#waiters,
253
+ versionHistory: this.#versionHistory,
254
+ requestedFiles,
255
+ evidenceRevision: () => this.#evidenceRevision,
256
+ failedFiles: () => new Set(Array.from(this.#failedUris).map(uriToFile)),
257
+ isRelatedUriTracked: (uri) => this.#openDocs.has(uri) || this.#versionHistory.has(uri),
258
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
259
+ invalidateEvidence: (uri) => {
260
+ this.#failedUris.add(uri);
261
+ const document = this.#openDocs.get(uri);
262
+ if (document) document.evidenceRevision = -1;
263
+ },
264
+ clearFile: (uri) => {
265
+ this.#failedUris.add(uri);
266
+ this.#unversionedPushSyncMoments.delete(uri);
267
+ this.#closedVersionedBarrier.add(uri);
268
+ clearTrackedDocumentState(this.#openDocs, this.#diagnosticStore, this.#waiters, uri);
269
+ },
270
+ markUnversionedSyncMoment: (uri) => this.#unversionedPushSyncMoments.set(uri, Date.now()),
271
+ clearFailedFile: (uri) => this.#failedUris.delete(uri),
272
+ options,
273
+ });
208
274
  }
209
275
 
210
- /** Sync one file and return its diagnostics after pull or push collection. */
211
- async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
276
+ async syncAndWaitForDiagnostics(
277
+ filePath: string,
278
+ content: string,
279
+ control?: CodeRequestControl,
280
+ ): Promise<CodeQueryResult<Diagnostic[]>> {
281
+ // Reject immediately when the request was already cancelled: no document
282
+ // synchronization or protocol traffic may start for a caller that no
283
+ // longer awaits a result.
284
+ throwIfCodeRequestInterrupted(control);
212
285
  const supportsPull = this.host.supportsPullDiagnostics();
213
- const observer = new DiagnosticObserver("sync-file", supportsPull);
286
+ const observer = new DiagnosticObserver("sync-file", supportsPull, control, {
287
+ server: this.host.server,
288
+ cwd: this.host.cwd,
289
+ file: relativeDiagnosticFile(this.host.cwd, filePath),
290
+ });
214
291
  const uri = fileToUri(filePath);
292
+ const cached = this.#diagnosticStore.get(uri);
293
+ const cachedDiagnostics = cached ? [...cached.diagnostics] : null;
215
294
  const syncStart = Date.now();
216
- this.didChange(filePath, content);
295
+ const openDocument = this.#openDocs.get(uri);
296
+ const contentUnchanged =
297
+ openDocument?.contentFingerprint === fingerprintDocumentContent(content);
298
+ const synchronizationCurrent = openDocument?.evidenceRevision === this.#evidenceRevision;
299
+ const cacheCurrent = hasCurrentDiagnosticEvidence(openDocument, cached, this.#evidenceRevision);
300
+ if (!contentUnchanged || !synchronizationCurrent || (cached !== undefined && !cacheCurrent)) {
301
+ synchronizeTrackedDocument({
302
+ uri,
303
+ content,
304
+ document: this.#openDocs.get(uri),
305
+ nextVersion: () => nextDocumentVersion(this.#versionHistory, uri),
306
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
307
+ evidenceRevision: this.#evidenceRevision,
308
+ waiters: this.#waiters,
309
+ sendNotification: (method, params) => this.host.sendNotification(method, params),
310
+ markUnversionedSyncMoment: () => this.#unversionedPushSyncMoments.set(uri, Date.now()),
311
+ clearFailedFile: () => this.#failedUris.delete(uri),
312
+ open: () => this.didOpen(filePath, content),
313
+ });
314
+ }
315
+ const synchronization = this.#openDocs.get(uri);
217
316
  observer.synchronized();
218
-
219
- if (supportsPull) {
220
- const remaining = DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart);
221
- try {
222
- if (remaining <= 0) observer.pullTimedOut();
223
- else if (await this.#pullDiagnosticsForUri(uri, remaining)) {
224
- observer.pullCompleted(1);
225
- return this.getDiagnostics(filePath);
226
- } else observer.pullFailed(undefined);
227
- } catch (error) {
228
- observer.pullFailed(error);
229
- }
317
+ if (!synchronization) {
318
+ return unavailableCodeQuery("The document could not be synchronized for diagnostics.");
230
319
  }
231
-
232
- const push = await this.#waitForDiagnostics(
320
+ if (
321
+ contentUnchanged &&
322
+ hasCurrentDiagnosticEvidence(synchronization, cached, this.#evidenceRevision)
323
+ ) {
324
+ observer.cacheReused(1);
325
+ return completedCodeQuery(cachedDiagnostics ?? []);
326
+ }
327
+ const request = {
233
328
  uri,
234
- Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)),
329
+ synchronizationId: synchronization.synchronizationId,
330
+ evidenceRevision: synchronization.evidenceRevision,
331
+ };
332
+ return this.#collectFileDiagnosticsWithReopenRetry(
333
+ {
334
+ filePath,
335
+ content,
336
+ uri,
337
+ supportsPull,
338
+ syncStart,
339
+ request,
340
+ cachedDiagnostics,
341
+ observer,
342
+ },
343
+ control,
235
344
  );
236
- observer.pushWaitCompleted(1, push);
237
- return this.getDiagnostics(filePath);
238
345
  }
239
346
 
240
- #resyncOpenDocuments(): void {
241
- for (const [uri, doc] of this.#openDocs) {
242
- const filePath = uriToFile(uri);
243
- try {
244
- if (!existsSync(filePath)) {
245
- this.#clearFileState(uri);
246
- this.#sendDidClose(uri);
247
- continue;
248
- }
249
- const content = readFileSync(filePath, "utf-8");
250
- doc.version++;
251
- this.#sendDidChange(uri, doc.version, content);
252
- } catch {
253
- // Keep the document open when a transient read fails.
347
+ /**
348
+ * Collect diagnostics for one synchronized document, retrying once through
349
+ * a reopen-resync on push-only routes when the first wait times out.
350
+ */
351
+ async #collectFileDiagnosticsWithReopenRetry(
352
+ options: {
353
+ filePath: string;
354
+ content: string;
355
+ uri: string;
356
+ supportsPull: boolean;
357
+ syncStart: number;
358
+ request: DiagnosticSynchronization;
359
+ cachedDiagnostics: Diagnostic[] | null;
360
+ observer: DiagnosticObserver;
361
+ },
362
+ control?: CodeRequestControl,
363
+ ): Promise<CodeQueryResult<Diagnostic[]>> {
364
+ const {
365
+ filePath,
366
+ content,
367
+ uri,
368
+ supportsPull,
369
+ syncStart,
370
+ request,
371
+ cachedDiagnostics,
372
+ observer,
373
+ } = options;
374
+ let pushOutcome: DiagnosticPushWaitOutcome | undefined;
375
+ let attemptRequest = request;
376
+ let attemptSyncStart = syncStart;
377
+ let attemptMaxWaitMs = DIAGNOSTIC_WAIT_MS;
378
+ let reopenedOnce = false;
379
+ let result: CodeQueryResult<Diagnostic[]>;
380
+ for (;;) {
381
+ result = await collectSynchronizedFileDiagnostics(
382
+ {
383
+ supportsPull,
384
+ syncStart: attemptSyncStart,
385
+ maxWaitMs: attemptMaxWaitMs,
386
+ request: attemptRequest,
387
+ cachedDiagnostics,
388
+ observer,
389
+ waiters: this.#waiters,
390
+ current: () => isCurrentSynchronization(this.#openDocs, attemptRequest),
391
+ freshPush: () =>
392
+ hasFreshPush(this.#diagnosticStore, attemptRequest, this.#evidenceRevision),
393
+ diagnostics: () => this.getDiagnostics(filePath),
394
+ pullDiagnostics: (timeoutMs, signal) =>
395
+ pullClientDiagnosticEvidenceFromHost({
396
+ host: this.host,
397
+ store: this.#diagnosticStore,
398
+ openDocuments: this.#openDocs,
399
+ currentEvidenceRevision: () => this.#evidenceRevision,
400
+ isRelatedUriTracked: (relatedUri) =>
401
+ this.#openDocs.has(relatedUri) || this.#versionHistory.has(relatedUri),
402
+ request: {
403
+ uri,
404
+ timeoutMs,
405
+ synchronizationId: attemptRequest.synchronizationId,
406
+ evidenceRevision: attemptRequest.evidenceRevision ?? this.#evidenceRevision,
407
+ signal,
408
+ deadline: control?.deadline,
409
+ operationId: control?.operationId,
410
+ },
411
+ }),
412
+ onPushWait: (outcome) => {
413
+ pushOutcome = outcome;
414
+ },
415
+ },
416
+ control,
417
+ );
418
+ // Reopen-resync fallback (R2): on push-only routes a document that
419
+ // timed out with no push stays unconfirmed — a clean file gets no
420
+ // push on didChange at all. Close and reopen it over the protocol so
421
+ // the server publishes on didOpen, then wait once more with a bounded
422
+ // budget. The cache entry and version history survive the reopen.
423
+ const document = this.#openDocs.get(uri);
424
+ if (
425
+ supportsPull ||
426
+ reopenedOnce ||
427
+ result.kind === "completed" ||
428
+ pushOutcome !== "timed-out" ||
429
+ !document ||
430
+ !isCurrentSynchronization(this.#openDocs, attemptRequest)
431
+ ) {
432
+ break;
254
433
  }
255
- }
256
- }
257
-
258
- async #pullDiagnosticsForOpenDocuments(syncStart: number, maxWaitMs: number): Promise<void> {
259
- const deadline = syncStart + maxWaitMs;
260
- const uris = Array.from(this.#openDocs.keys());
261
- const results = await Promise.allSettled(
262
- uris.map(async (uri) => {
263
- const remaining = deadline - Date.now();
264
- if (remaining <= 0) throw new Error("pull diagnostic timeout");
265
- return this.#pullDiagnosticsForUri(uri, remaining);
266
- }),
267
- );
268
-
269
- const anySuccess = results.some((result) => result.status === "fulfilled" && result.value);
270
- const failures = results.filter((result) => result.status === "rejected");
271
- if ((failures.length > 0 || !anySuccess) && uris.length > 0) {
272
- throw new DiagnosticPullError(failures.some((result) => isDiagnosticTimeout(result.reason)));
273
- }
274
- }
275
-
276
- async #pullDiagnosticsForUri(uri: string, timeoutMs: number): Promise<boolean> {
277
- const previousResultId = this.#diagnosticStore.get(uri)?.resultId;
278
- const report = await this.host.pullDocumentDiagnostics(uri, previousResultId, timeoutMs);
279
- if (!report) return false;
280
- this.#applyPullReport(uri, report);
281
- return true;
282
- }
283
-
284
- #applyPullReport(uri: string, report: DocumentDiagnosticReport): void {
285
- if (report.kind === "full") {
286
- this.#diagnosticStore.set(uri, {
287
- diagnostics: report.items,
288
- receivedAt: Date.now(),
289
- resultId: report.resultId,
290
- });
291
- } else {
292
- const current = this.#diagnosticStore.get(uri);
293
- if (current) current.resultId = report.resultId;
294
- }
295
-
296
- for (const [relatedUri, relatedReport] of Object.entries(report.relatedDocuments ?? {})) {
297
- if (relatedReport.kind !== "full") continue;
298
- this.#diagnosticStore.set(relatedUri, {
299
- diagnostics: relatedReport.items,
300
- receivedAt: Date.now(),
301
- resultId: relatedReport.resultId,
434
+ reopenDocument({
435
+ uri,
436
+ content,
437
+ document,
438
+ languageId: detectLanguageId(filePath),
439
+ nextVersion: () => nextDocumentVersion(this.#versionHistory, uri),
440
+ nextSynchronizationId: () => ++this.#nextSynchronizationId,
441
+ evidenceRevision: this.#evidenceRevision,
442
+ waiters: this.#waiters,
443
+ sendNotification: (method, params) => this.host.sendNotification(method, params),
444
+ markUnversionedSyncMoment: () => this.#unversionedPushSyncMoments.set(uri, Date.now()),
302
445
  });
303
- }
304
- }
305
-
306
- async #waitForDiagnosticSettle(
307
- syncStart: number,
308
- maxWaitMs: number,
309
- quietMs: number,
310
- ): Promise<DiagnosticSettleResult> {
311
- const deadline = syncStart + maxWaitMs;
312
- while (Date.now() < deadline) {
313
- const observedAt = this.#lastDiagnosticReceivedAfter(syncStart);
314
- const elapsed = Date.now() - (observedAt || syncStart);
315
- if (elapsed >= quietMs) {
316
- return {
317
- outcome: "quiet",
318
- freshness: observedAt > 0 ? "observed" : "not-observed",
319
- };
320
- }
321
- await new Promise((resolve) =>
322
- setTimeout(resolve, Math.min(quietMs - elapsed, deadline - Date.now(), 50)),
323
- );
324
- }
325
- return {
326
- outcome: "timed-out",
327
- freshness: this.#lastDiagnosticReceivedAfter(syncStart) > 0 ? "observed" : "not-observed",
328
- };
329
- }
330
-
331
- #lastDiagnosticReceivedAfter(afterTime: number): number {
332
- let latest = 0;
333
- for (const entry of this.#diagnosticStore.values()) {
334
- if (entry.receivedAt > afterTime && entry.receivedAt > latest) latest = entry.receivedAt;
335
- }
336
- return latest;
337
- }
338
-
339
- #sendDidChange(uri: string, version: number, content: string): void {
340
- this.host.sendNotification("textDocument/didChange", {
341
- textDocument: { uri, version } satisfies VersionedTextDocumentIdentifier,
342
- contentChanges: [{ text: content }],
343
- });
344
- }
345
-
346
- #sendDidClose(uri: string): void {
347
- this.host.sendNotification("textDocument/didClose", {
348
- textDocument: { uri } satisfies TextDocumentIdentifier,
349
- });
350
- }
351
-
352
- #clearFileState(uri: string): void {
353
- this.#openDocs.delete(uri);
354
- this.#diagnosticStore.delete(uri);
355
- this.#releaseDiagnosticWaiters(uri);
356
- }
357
-
358
- #waitForDiagnostics(uri: string, timeoutMs: number): Promise<DiagnosticPushWaitOutcome> {
359
- if (timeoutMs <= 0) return Promise.resolve("timed-out");
360
-
361
- return new Promise<DiagnosticPushWaitOutcome>((resolve) => {
362
- const waiter: DiagnosticWaiter = (outcome) => {
363
- clearTimeout(timer);
364
- this.#removeDiagnosticWaiter(uri, waiter);
365
- resolve(outcome);
446
+ observer.reopened(1);
447
+ reopenedOnce = true;
448
+ attemptRequest = {
449
+ uri,
450
+ synchronizationId: document.synchronizationId,
451
+ evidenceRevision: document.evidenceRevision,
366
452
  };
367
- const timer = setTimeout(() => {
368
- this.#removeDiagnosticWaiter(uri, waiter);
369
- resolve("timed-out");
370
- }, timeoutMs);
371
- const waiters = this.#diagnosticWaiters.get(uri) ?? [];
372
- waiters.push(waiter);
373
- this.#diagnosticWaiters.set(uri, waiters);
374
- });
375
- }
376
-
377
- #removeDiagnosticWaiter(uri: string, waiter: DiagnosticWaiter): void {
378
- const waiters = this.#diagnosticWaiters.get(uri);
379
- if (!waiters) return;
380
- const next = waiters.filter((entry) => entry !== waiter);
381
- if (next.length > 0) this.#diagnosticWaiters.set(uri, next);
382
- else this.#diagnosticWaiters.delete(uri);
383
- }
384
-
385
- #releaseAllDiagnosticWaiters(): void {
386
- for (const uri of Array.from(this.#diagnosticWaiters.keys())) {
387
- this.#releaseDiagnosticWaiters(uri);
453
+ attemptSyncStart = Date.now();
454
+ attemptMaxWaitMs = REOPEN_EVIDENCE_WAIT_MS;
388
455
  }
456
+ observer.pushWaitCompleted(1, pushOutcome ?? "timed-out");
457
+ return result;
389
458
  }
459
+ }
390
460
 
391
- #releaseDiagnosticWaiters(uri: string, outcome: "published" | "released" = "released"): void {
392
- const waiters = this.#diagnosticWaiters.get(uri);
393
- if (!waiters) return;
394
- this.#diagnosticWaiters.delete(uri);
395
- for (const waiter of waiters) waiter(outcome);
396
- }
461
+ /** Return a workspace-relative diagnostic file path for telemetry identity. */
462
+ function relativeDiagnosticFile(cwd: string | undefined, filePath: string): string | undefined {
463
+ if (cwd === undefined) return undefined;
464
+ return path.relative(cwd, path.resolve(cwd, filePath));
397
465
  }