@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
@@ -0,0 +1,491 @@
1
+ // biome-ignore-all lint/style/noExcessiveLinesPerFile: refresh orchestration, pull collection, and the reopen fallback stay in one cohesive module.
2
+ import { readFileSync } from "node:fs";
3
+ import {
4
+ type CodeRequestControl,
5
+ isCodeRequestInterruption,
6
+ throwIfCodeRequestInterrupted,
7
+ } from "@mrclrchtr/supi-code-runtime/api";
8
+ import type { TextDocumentIdentifier } from "../config/types.ts";
9
+ import {
10
+ type DiagnosticEvidenceSummary,
11
+ summarizeDiagnosticEvidence,
12
+ } from "../diagnostics/evidence.ts";
13
+ import { detectLanguageId, fileToUri, uriToFile } from "../utils.ts";
14
+ import {
15
+ type DiagnosticCacheEntry,
16
+ type DiagnosticSynchronization,
17
+ hasFreshEvidence,
18
+ hasFreshPush,
19
+ isCurrentSynchronization,
20
+ latestFreshEvidenceReceivedAt,
21
+ nextDocumentVersion,
22
+ raceDiagnosticPull,
23
+ } from "./client-diagnostic-evidence.ts";
24
+ import type { ClientDiagnosticsHost } from "./client-diagnostic-host.ts";
25
+ import { pullDiagnosticEvidence } from "./client-diagnostic-pull.ts";
26
+ import type { DiagnosticPullRequest } from "./client-diagnostic-request.ts";
27
+ import {
28
+ DiagnosticObserver,
29
+ DiagnosticPullError,
30
+ isDiagnosticTimeout,
31
+ } from "./client-diagnostic-timing.ts";
32
+ import type { DiagnosticStateWait, DiagnosticWaitRegistry } from "./client-diagnostic-waiters.ts";
33
+ import type { OpenDocumentState } from "./client-document-state.ts";
34
+ import {
35
+ type ResynchronizeDocumentsResult,
36
+ reopenDocument,
37
+ resynchronizeOpenDocuments,
38
+ } from "./client-document-sync.ts";
39
+ import { getDiagnosticFileState } from "./client-file-state.ts";
40
+
41
+ /** Send the protocol close notification for one removed document. */
42
+ export function sendDidCloseNotification(
43
+ host: Pick<ClientDiagnosticsHost, "sendNotification">,
44
+ uri: string,
45
+ ): void {
46
+ host.sendNotification("textDocument/didClose", {
47
+ textDocument: { uri } satisfies TextDocumentIdentifier,
48
+ });
49
+ }
50
+
51
+ /** Build final document coverage after one client refresh attempt. */
52
+ export function buildDiagnosticRefreshEvidence(options: {
53
+ requestedFiles: readonly string[];
54
+ resynchronization: ResynchronizeDocumentsResult;
55
+ /** Synchronizations that prove evidence, after reopen-resync updates. */
56
+ synchronizations: readonly DiagnosticSynchronization[];
57
+ failedPullUris: ReadonlySet<string>;
58
+ failedFiles: ReadonlySet<string>;
59
+ failedResynchronizations: ReadonlySet<string>;
60
+ currentEvidenceRevision: number;
61
+ openDocuments: ReadonlyMap<string, unknown>;
62
+ diagnosticStore: ReadonlyMap<string, DiagnosticCacheEntry>;
63
+ }): DiagnosticEvidenceSummary {
64
+ const synchronizationByFile = new Map(
65
+ options.synchronizations.map((item) => [uriToFile(item.uri), item]),
66
+ );
67
+ const removedFiles = new Set(options.resynchronization.removedFiles);
68
+ const failedFiles = new Set(options.resynchronization.failedFiles);
69
+ const documents = options.requestedFiles.map((file) => {
70
+ const uri = fileToUri(file);
71
+ const synchronization = synchronizationByFile.get(file);
72
+ if (removedFiles.has(file) || getDiagnosticFileState(file) === "removed") {
73
+ return { file, status: "removed" as const };
74
+ }
75
+ if (
76
+ failedFiles.has(file) ||
77
+ options.failedFiles.has(file) ||
78
+ options.failedResynchronizations.has(file)
79
+ ) {
80
+ return { file, status: "failed" as const };
81
+ }
82
+ if (!options.openDocuments.has(uri)) {
83
+ return { file, status: "unconfirmed" as const };
84
+ }
85
+ if (
86
+ synchronization &&
87
+ hasFreshEvidence(options.diagnosticStore, synchronization, options.currentEvidenceRevision)
88
+ ) {
89
+ return { file, status: "confirmed" as const };
90
+ }
91
+ return {
92
+ file,
93
+ status: options.failedPullUris.has(uri) ? ("failed" as const) : ("unconfirmed" as const),
94
+ };
95
+ });
96
+ return summarizeDiagnosticEvidence(documents);
97
+ }
98
+
99
+ /** Pull one document and reject evidence from a stale client generation. */
100
+ export function pullClientDiagnosticEvidence(
101
+ options: Omit<DiagnosticPullRequest, "previousResultId"> & {
102
+ store: Map<string, DiagnosticCacheEntry>;
103
+ synchronizationId?: number;
104
+ evidenceRevision: number;
105
+ currentRevision(): number;
106
+ isCurrentSynchronization(): boolean;
107
+ isRelatedUriTracked(uri: string): boolean;
108
+ pull(
109
+ request: DiagnosticPullRequest,
110
+ ): Promise<import("../config/types.ts").DocumentDiagnosticReport | null>;
111
+ },
112
+ ): Promise<boolean> {
113
+ return pullDiagnosticEvidence(options);
114
+ }
115
+
116
+ /** Pull one document through a client's host and preserve generation checks. */
117
+ export function pullClientDiagnosticEvidenceFromHost(options: {
118
+ host: Pick<ClientDiagnosticsHost, "pullDocumentDiagnostics">;
119
+ store: Map<string, DiagnosticCacheEntry>;
120
+ openDocuments: ReadonlyMap<string, { synchronizationId: number; evidenceRevision?: number }>;
121
+ currentEvidenceRevision(): number;
122
+ isRelatedUriTracked(uri: string): boolean;
123
+ request: Omit<DiagnosticPullRequest, "previousResultId"> & {
124
+ synchronizationId?: number;
125
+ evidenceRevision?: number;
126
+ };
127
+ }): Promise<boolean> {
128
+ return pullClientDiagnosticEvidence({
129
+ store: options.store,
130
+ ...options.request,
131
+ evidenceRevision: options.request.evidenceRevision ?? options.currentEvidenceRevision(),
132
+ currentRevision: options.currentEvidenceRevision,
133
+ isCurrentSynchronization: () =>
134
+ options.request.synchronizationId === undefined ||
135
+ isCurrentSynchronization(options.openDocuments, {
136
+ uri: options.request.uri,
137
+ synchronizationId: options.request.synchronizationId,
138
+ evidenceRevision: options.request.evidenceRevision,
139
+ }),
140
+ isRelatedUriTracked: options.isRelatedUriTracked,
141
+ pull: (request) => options.host.pullDocumentDiagnostics(request),
142
+ });
143
+ }
144
+
145
+ /** Collect pull evidence for every synchronized document in one refresh. */
146
+ export async function pullDiagnosticsForOpenDocuments(options: {
147
+ requests: readonly DiagnosticSynchronization[];
148
+ syncStart: number;
149
+ maxWaitMs: number;
150
+ signal?: AbortSignal;
151
+ deadline?: number;
152
+ operationId?: string;
153
+ currentEvidenceRevision: () => number;
154
+ openDocuments: ReadonlyMap<string, { evidenceRevision: number; synchronizationId: number }>;
155
+ diagnosticStore: ReadonlyMap<string, DiagnosticCacheEntry>;
156
+ waitForChange: () => DiagnosticStateWait;
157
+ pullDiagnostics: (options: {
158
+ request: DiagnosticSynchronization;
159
+ timeoutMs: number;
160
+ signal: AbortSignal;
161
+ operationId?: string;
162
+ deadline?: number;
163
+ }) => Promise<boolean>;
164
+ }): Promise<void> {
165
+ const deadline = options.syncStart + options.maxWaitMs;
166
+ const results = await Promise.allSettled(
167
+ options.requests.map(async (request) => {
168
+ const remaining = deadline - Date.now();
169
+ if (remaining <= 0) throw new Error("pull diagnostic timeout");
170
+ const pullController = new AbortController();
171
+ // Link the caller's cancellation to this document's pull controller so
172
+ // an in-flight pull receives protocol cancellation and stops promptly.
173
+ const onAbort = () => pullController.abort(options.signal?.reason);
174
+ if (options.signal?.aborted) onAbort();
175
+ else options.signal?.addEventListener("abort", onAbort, { once: true });
176
+ try {
177
+ const outcome = await raceDiagnosticPull({
178
+ pull: options.pullDiagnostics({
179
+ request,
180
+ timeoutMs: remaining,
181
+ signal: pullController.signal,
182
+ operationId: options.operationId,
183
+ deadline: options.deadline,
184
+ }),
185
+ waitForChange: options.waitForChange,
186
+ freshPush: () =>
187
+ hasFreshPush(options.diagnosticStore, request, options.currentEvidenceRevision()),
188
+ current: () => isCurrentSynchronization(options.openDocuments, request),
189
+ });
190
+ if (outcome !== "pull") pullController.abort();
191
+ return outcome === "pull";
192
+ } finally {
193
+ options.signal?.removeEventListener("abort", onAbort);
194
+ }
195
+ }),
196
+ );
197
+
198
+ // A cancelled refresh must not degrade into failed coverage evidence:
199
+ // the caller no longer awaits a result.
200
+ throwIfCodeRequestInterrupted({ signal: options.signal, deadline: options.deadline });
201
+
202
+ const incomplete = results.some((result) => result.status === "rejected" || !result.value);
203
+ if (incomplete && options.requests.length > 0) {
204
+ const failedUris = options.requests.flatMap((request, index) => {
205
+ const result = results[index];
206
+ return result?.status === "rejected" && !isDiagnosticTimeout(result.reason)
207
+ ? [request.uri]
208
+ : [];
209
+ });
210
+ throw new DiagnosticPullError(
211
+ results.some((result) => result.status === "rejected" && isDiagnosticTimeout(result.reason)),
212
+ failedUris,
213
+ );
214
+ }
215
+ }
216
+
217
+ interface ClientDiagnosticRefreshOptions {
218
+ readonly host: ClientDiagnosticsHost;
219
+ readonly openDocuments: Map<string, OpenDocumentState>;
220
+ readonly diagnosticStore: Map<string, DiagnosticCacheEntry>;
221
+ readonly waiters: DiagnosticWaitRegistry;
222
+ readonly versionHistory: Map<string, number>;
223
+ readonly requestedFiles: readonly string[];
224
+ readonly evidenceRevision: () => number;
225
+ readonly failedFiles: () => ReadonlySet<string>;
226
+ readonly isRelatedUriTracked: (uri: string) => boolean;
227
+ readonly nextSynchronizationId: () => number;
228
+ readonly clearFile: (uri: string) => void;
229
+ readonly invalidateEvidence: (uri: string) => void;
230
+ readonly markUnversionedSyncMoment: (uri: string) => void;
231
+ readonly clearFailedFile: (uri: string) => void;
232
+ readonly options: { maxWaitMs?: number; quietMs?: number } & CodeRequestControl;
233
+ }
234
+
235
+ /** Refresh one client and return exact document evidence for that attempt. */
236
+ export async function refreshClientOpenDiagnostics(
237
+ options: ClientDiagnosticRefreshOptions,
238
+ ): Promise<DiagnosticEvidenceSummary> {
239
+ // Reject immediately when the request was already cancelled: no document
240
+ // resynchronization or protocol traffic may start for a pass the caller
241
+ // no longer awaits.
242
+ throwIfCodeRequestInterrupted(options.options);
243
+ const supportsPull = options.host.supportsPullDiagnostics();
244
+ const observer = new DiagnosticObserver("refresh-open", supportsPull, options.options, {
245
+ server: options.host.server,
246
+ cwd: options.host.cwd,
247
+ });
248
+ if (!options.host.isOperational()) {
249
+ observer.skipped(options.requestedFiles.length);
250
+ return summarizeDiagnosticEvidence(
251
+ options.requestedFiles.map((file) => ({
252
+ file,
253
+ status:
254
+ getDiagnosticFileState(file) === "removed" ? ("removed" as const) : ("failed" as const),
255
+ })),
256
+ );
257
+ }
258
+
259
+ const maxWaitMs = options.options.maxWaitMs ?? 3_000;
260
+ const quietMs = options.options.quietMs ?? 200;
261
+ const syncStart = Date.now();
262
+ const resynchronization = resynchronizeOpenDocuments({
263
+ openDocuments: options.openDocuments,
264
+ waiters: options.waiters,
265
+ nextVersion: (uri) => nextDocumentVersion(options.versionHistory, uri),
266
+ nextSynchronizationId: options.nextSynchronizationId,
267
+ evidenceRevision: options.evidenceRevision(),
268
+ sendNotification: (method, params) => options.host.sendNotification(method, params),
269
+ uriToFile,
270
+ clearFile: options.clearFile,
271
+ invalidateEvidence: options.invalidateEvidence,
272
+ markUnversionedSyncMoment: options.markUnversionedSyncMoment,
273
+ clearFailedFile: options.clearFailedFile,
274
+ });
275
+ let synchronizations = resynchronization.synchronizations;
276
+ const settleEpoch = options.waiters.settleEpoch;
277
+ observer.synchronized();
278
+ const documentCount = synchronizations.length;
279
+ let failedPullUris: ReadonlySet<string> = new Set();
280
+ const buildEvidence = () =>
281
+ buildDiagnosticRefreshEvidence({
282
+ requestedFiles: options.requestedFiles,
283
+ resynchronization,
284
+ synchronizations,
285
+ failedPullUris,
286
+ failedFiles: options.failedFiles(),
287
+ failedResynchronizations: new Set(resynchronization.failedFiles),
288
+ currentEvidenceRevision: options.evidenceRevision(),
289
+ openDocuments: options.openDocuments,
290
+ diagnosticStore: options.diagnosticStore,
291
+ });
292
+ if (documentCount === 0) {
293
+ observer.skipped(0);
294
+ return buildEvidence();
295
+ }
296
+
297
+ if (supportsPull) {
298
+ const pull = await collectPullEvidenceForRefresh({
299
+ options,
300
+ synchronizations,
301
+ syncStart,
302
+ maxWaitMs,
303
+ observer,
304
+ });
305
+ failedPullUris = new Set(pull.failedPullUris);
306
+ if (pull.completed) return buildEvidence();
307
+ }
308
+
309
+ let finalSettle = await options.waiters.waitForSettle(
310
+ {
311
+ syncStart,
312
+ maxWaitMs,
313
+ quietMs,
314
+ settleEpoch,
315
+ isComplete: () =>
316
+ synchronizations.every((item) =>
317
+ hasFreshEvidence(options.diagnosticStore, item, options.evidenceRevision()),
318
+ ),
319
+ latestReceived: () =>
320
+ latestFreshEvidenceReceivedAt(
321
+ options.diagnosticStore,
322
+ synchronizations,
323
+ options.evidenceRevision(),
324
+ ),
325
+ },
326
+ options.options,
327
+ );
328
+ if (!supportsPull && finalSettle.outcome === "timed-out") {
329
+ const reopen = await reopenUnconfirmedDocuments({
330
+ options,
331
+ synchronizations,
332
+ maxWaitMs,
333
+ quietMs,
334
+ observer,
335
+ });
336
+ if (reopen.performed) {
337
+ synchronizations = reopen.synchronizations;
338
+ finalSettle = await options.waiters.waitForSettle(
339
+ {
340
+ syncStart: reopen.startedAt,
341
+ maxWaitMs: reopen.budgetMs,
342
+ quietMs,
343
+ settleEpoch: options.waiters.settleEpoch,
344
+ isComplete: () =>
345
+ synchronizations.every((item) =>
346
+ hasFreshEvidence(options.diagnosticStore, item, options.evidenceRevision()),
347
+ ),
348
+ latestReceived: () =>
349
+ latestFreshEvidenceReceivedAt(
350
+ options.diagnosticStore,
351
+ synchronizations,
352
+ options.evidenceRevision(),
353
+ ),
354
+ },
355
+ options.options,
356
+ );
357
+ }
358
+ }
359
+ observer.pushSettled(documentCount, finalSettle);
360
+ // A cancelled settle must not publish evidence the caller no longer awaits.
361
+ throwIfCodeRequestInterrupted(options.options);
362
+ return buildEvidence();
363
+ }
364
+
365
+ /**
366
+ * Pull diagnostic evidence for every synchronized document, or fall through
367
+ * to the push settle path when any pull fails. An interruption during the
368
+ * pull phase stops the refresh instead of degrading into failed coverage.
369
+ */
370
+ async function collectPullEvidenceForRefresh(options: {
371
+ options: ClientDiagnosticRefreshOptions;
372
+ synchronizations: readonly DiagnosticSynchronization[];
373
+ syncStart: number;
374
+ maxWaitMs: number;
375
+ observer: DiagnosticObserver;
376
+ }): Promise<{ completed: boolean; failedPullUris: ReadonlySet<string> }> {
377
+ const { options: refresh, synchronizations, syncStart, maxWaitMs, observer } = options;
378
+ try {
379
+ await pullDiagnosticsForOpenDocuments({
380
+ requests: synchronizations,
381
+ syncStart,
382
+ maxWaitMs,
383
+ signal: refresh.options.signal,
384
+ deadline: refresh.options.deadline,
385
+ operationId: refresh.options.operationId,
386
+ currentEvidenceRevision: refresh.evidenceRevision,
387
+ openDocuments: refresh.openDocuments,
388
+ diagnosticStore: refresh.diagnosticStore,
389
+ waitForChange: () => refresh.waiters.waitForChange(),
390
+ pullDiagnostics: (pullOptions) =>
391
+ pullClientDiagnosticEvidenceFromHost({
392
+ host: refresh.host,
393
+ store: refresh.diagnosticStore,
394
+ openDocuments: refresh.openDocuments,
395
+ currentEvidenceRevision: refresh.evidenceRevision,
396
+ isRelatedUriTracked: refresh.isRelatedUriTracked,
397
+ request: {
398
+ uri: pullOptions.request.uri,
399
+ timeoutMs: pullOptions.timeoutMs,
400
+ synchronizationId: pullOptions.request.synchronizationId,
401
+ evidenceRevision:
402
+ refresh.openDocuments.get(pullOptions.request.uri)?.evidenceRevision ??
403
+ refresh.evidenceRevision(),
404
+ signal: pullOptions.signal,
405
+ deadline: pullOptions.deadline,
406
+ operationId: pullOptions.operationId,
407
+ },
408
+ }),
409
+ });
410
+ observer.pullCompleted(synchronizations.length);
411
+ return { completed: true, failedPullUris: new Set() };
412
+ } catch (error) {
413
+ observer.pullFailed(error);
414
+ if (error instanceof DiagnosticPullError) {
415
+ return { completed: false, failedPullUris: new Set(error.failedUris) };
416
+ }
417
+ if (isCodeRequestInterruption(error, refresh.options)) throw error;
418
+ return { completed: false, failedPullUris: new Set() };
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Reopen-resync fallback (R2): on push-only routes an open document that
424
+
425
+ * stays unconfirmed after the settle window receives no further push — a
426
+ * clean file gets no push on didChange at all. Close and reopen each
427
+ * unconfirmed document so the server publishes on didOpen, then settle
428
+ * again within a bounded second window. The cache entry and version
429
+ * history survive the reopen; other documents keep their server state.
430
+ */
431
+ async function reopenUnconfirmedDocuments(options: {
432
+ options: ClientDiagnosticRefreshOptions;
433
+ synchronizations: readonly DiagnosticSynchronization[];
434
+ maxWaitMs: number;
435
+ quietMs: number;
436
+ observer: DiagnosticObserver;
437
+ }): Promise<{
438
+ performed: boolean;
439
+ startedAt: number;
440
+ budgetMs: number;
441
+ synchronizations: DiagnosticSynchronization[];
442
+ }> {
443
+ const { options: refresh, synchronizations, maxWaitMs, quietMs, observer } = options;
444
+ const startedAt = Date.now();
445
+ const budgetMs = Math.min(maxWaitMs, quietMs * 4);
446
+ const unconfirmed = synchronizations.filter(
447
+ (item) => !hasFreshEvidence(refresh.diagnosticStore, item, refresh.evidenceRevision()),
448
+ );
449
+ const reopenedSynchronizations: DiagnosticSynchronization[] = [];
450
+ for (const item of unconfirmed) {
451
+ const document = refresh.openDocuments.get(item.uri);
452
+ if (!document) continue;
453
+ const filePath = uriToFile(item.uri);
454
+ let content: string;
455
+ try {
456
+ content = readFileSync(filePath, "utf-8");
457
+ } catch {
458
+ // The file disappeared mid-refresh; keep the document as-is and
459
+ // report its current unconfirmed coverage.
460
+ continue;
461
+ }
462
+ reopenDocument({
463
+ uri: item.uri,
464
+ content,
465
+ document,
466
+ languageId: detectLanguageId(filePath),
467
+ nextVersion: () => nextDocumentVersion(refresh.versionHistory, item.uri),
468
+ nextSynchronizationId: refresh.nextSynchronizationId,
469
+ evidenceRevision: refresh.evidenceRevision(),
470
+ waiters: refresh.waiters,
471
+ sendNotification: (method, params) => refresh.host.sendNotification(method, params),
472
+ markUnversionedSyncMoment: () => refresh.markUnversionedSyncMoment(item.uri),
473
+ });
474
+ reopenedSynchronizations.push({
475
+ uri: item.uri,
476
+ synchronizationId: document.synchronizationId,
477
+ evidenceRevision: document.evidenceRevision,
478
+ });
479
+ }
480
+ if (reopenedSynchronizations.length === 0) {
481
+ return { performed: false, startedAt, budgetMs, synchronizations: [...synchronizations] };
482
+ }
483
+ observer.reopened(reopenedSynchronizations.length);
484
+ const reopenedByUri = new Map(reopenedSynchronizations.map((item) => [item.uri, item]));
485
+ return {
486
+ performed: true,
487
+ startedAt,
488
+ budgetMs,
489
+ synchronizations: synchronizations.map((item) => reopenedByUri.get(item.uri) ?? item),
490
+ };
491
+ }
@@ -0,0 +1,9 @@
1
+ /** Sanitized ownership and transport control for one explicit diagnostic pull. */
2
+ export interface DiagnosticPullRequest {
3
+ readonly uri: string;
4
+ readonly previousResultId: string | undefined;
5
+ readonly timeoutMs: number;
6
+ readonly signal?: AbortSignal;
7
+ readonly deadline?: number;
8
+ readonly operationId?: string;
9
+ }
@@ -1,8 +1,10 @@
1
+ import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
1
2
  import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
3
+ import { boundCwd, truncateIdentity } from "../debug-telemetry.ts";
2
4
 
3
- type DiagnosticCollection = "fallback" | "none" | "pull" | "push";
5
+ type DiagnosticCollection = "cache" | "fallback" | "none" | "pull" | "push";
4
6
  type DiagnosticFreshness = "not-observed" | "observed";
5
- type DiagnosticOutcome = "completed" | "skipped" | "timed-out";
7
+ type DiagnosticOutcome = "completed" | "incomplete" | "skipped" | "timed-out";
6
8
  type DiagnosticPullOutcome = "completed" | "failed" | "not-supported" | "not-used" | "timed-out";
7
9
  type DiagnosticPushOutcome = "not-used" | "published" | "released" | "settled" | "timed-out";
8
10
  type DiagnosticSettleOutcome = "not-used" | "published" | "quiet" | "released" | "timed-out";
@@ -10,7 +12,7 @@ type DiagnosticTimingOperation = "refresh-open" | "sync-file";
10
12
 
11
13
  /** Result of waiting for a quiet push-diagnostic window. */
12
14
  export interface DiagnosticSettleResult {
13
- readonly outcome: "quiet" | "timed-out";
15
+ readonly outcome: "quiet" | "released" | "timed-out";
14
16
  readonly freshness: DiagnosticFreshness;
15
17
  }
16
18
 
@@ -25,30 +27,47 @@ interface DiagnosticTimingData {
25
27
  readonly outcome: DiagnosticOutcome;
26
28
  readonly pull: DiagnosticPullOutcome;
27
29
  readonly push: DiagnosticPushOutcome;
30
+ readonly reopen: number;
28
31
  readonly settle: DiagnosticSettleOutcome;
29
32
  readonly timedOut: boolean;
30
33
  }
31
34
 
32
- /** Internal pull failure that retains only whether a timeout occurred. */
35
+ /** Internal pull failure that retains timeout state and failed document URIs. */
33
36
  export class DiagnosticPullError extends Error {
34
- constructor(readonly timedOut: boolean) {
37
+ constructor(
38
+ readonly timedOut: boolean,
39
+ readonly failedUris: readonly string[] = [],
40
+ ) {
35
41
  super("pull diagnostics incomplete");
36
42
  }
37
43
  }
38
44
 
45
+ /** Bounded identity for one diagnostic timing observation. */
46
+ export interface DiagnosticTimingIdentity {
47
+ /** Configured server name. */
48
+ readonly server?: string;
49
+ /** Absolute workspace root. */
50
+ readonly cwd?: string;
51
+ /** Workspace-relative file path; sync-file operations only. */
52
+ readonly file?: string;
53
+ }
54
+
39
55
  /**
40
- * Record one diagnostic operation without document identifiers or diagnostic text.
56
+ * Record one diagnostic operation without diagnostic text or document content.
41
57
  *
42
58
  * The observer owns result classification so diagnostic control flow does not
43
- * duplicate the event shape.
59
+ * duplicate the event shape. Identity is bounded and workspace-relative.
44
60
  */
45
61
  export class DiagnosticObserver {
46
62
  readonly #timer = startDebugTimer();
47
63
  #pull: "failed" | "not-supported" | "timed-out";
64
+ #reopened = 0;
48
65
 
49
66
  constructor(
50
67
  readonly operation: DiagnosticTimingOperation,
51
68
  readonly supportsPull: boolean,
69
+ readonly control?: CodeRequestControl,
70
+ readonly identity?: DiagnosticTimingIdentity,
52
71
  ) {
53
72
  this.#pull = supportsPull ? "failed" : "not-supported";
54
73
  }
@@ -66,6 +85,22 @@ export class DiagnosticObserver {
66
85
  outcome: "skipped",
67
86
  pull: "not-used",
68
87
  push: "not-used",
88
+ reopen: 0,
89
+ settle: "not-used",
90
+ timedOut: false,
91
+ });
92
+ }
93
+
94
+ cacheReused(documentCount: number): void {
95
+ this.#finish({
96
+ collection: "cache",
97
+ documentCount,
98
+ fallback: false,
99
+ freshness: "observed",
100
+ outcome: "completed",
101
+ pull: "not-used",
102
+ push: "not-used",
103
+ reopen: 0,
69
104
  settle: "not-used",
70
105
  timedOut: false,
71
106
  });
@@ -81,6 +116,7 @@ export class DiagnosticObserver {
81
116
  outcome: "completed",
82
117
  pull: "completed",
83
118
  push: "not-used",
119
+ reopen: 0,
84
120
  settle: "not-used",
85
121
  timedOut: false,
86
122
  },
@@ -98,17 +134,25 @@ export class DiagnosticObserver {
98
134
  this.#timer.mark("pull");
99
135
  }
100
136
 
137
+ /** Record that the reopen-resync fallback re-opened unconfirmed documents. */
138
+ reopened(count: number): void {
139
+ this.#reopened += count;
140
+ this.#timer.mark("reopen");
141
+ }
142
+
101
143
  pushSettled(documentCount: number, settle: DiagnosticSettleResult): void {
102
144
  const timedOut = settle.outcome === "timed-out";
145
+ const completed = settle.outcome === "quiet" && settle.freshness === "observed";
103
146
  this.#finish(
104
147
  {
105
148
  collection: this.supportsPull ? "fallback" : "push",
106
149
  documentCount,
107
150
  fallback: this.supportsPull,
108
151
  freshness: settle.freshness,
109
- outcome: timedOut ? "timed-out" : "completed",
152
+ outcome: completed ? "completed" : timedOut ? "timed-out" : "incomplete",
110
153
  pull: this.#pull,
111
- push: timedOut ? "timed-out" : "settled",
154
+ push: timedOut ? "timed-out" : settle.outcome === "released" ? "released" : "settled",
155
+ reopen: this.#reopened,
112
156
  settle: settle.outcome,
113
157
  timedOut: timedOut || this.#pull === "timed-out",
114
158
  },
@@ -124,9 +168,10 @@ export class DiagnosticObserver {
124
168
  documentCount,
125
169
  fallback: this.supportsPull,
126
170
  freshness: push === "published" ? "observed" : "not-observed",
127
- outcome: timedOut ? "timed-out" : "completed",
171
+ outcome: push === "published" ? "completed" : timedOut ? "timed-out" : "incomplete",
128
172
  pull: this.#pull,
129
173
  push,
174
+ reopen: this.#reopened,
130
175
  settle: push,
131
176
  timedOut: timedOut || this.#pull === "timed-out",
132
177
  },
@@ -135,13 +180,28 @@ export class DiagnosticObserver {
135
180
  }
136
181
 
137
182
  #finish(data: DiagnosticTimingData, finalPhase?: "pull" | "push-settle" | "synchronize"): void {
183
+ const { reopen, ...observation } = data;
138
184
  this.#timer.finish(
139
185
  () => ({
186
+ operationId: this.control?.operationId,
140
187
  source: "lsp",
141
188
  level: "debug",
142
189
  category: "diagnostics.timing",
143
190
  message: `LSP diagnostic ${this.operation} ${data.outcome}`,
144
- data: { operation: this.operation, ...data },
191
+ cwd: boundCwd(this.identity?.cwd),
192
+ data: {
193
+ operation: this.operation,
194
+ ...observation,
195
+ // Reopen fallback usage is recorded only when it happened; the
196
+ // default event shape stays stable for consumers of #322 telemetry.
197
+ ...(reopen > 0 ? { reopen } : {}),
198
+ ...(this.identity?.server !== undefined
199
+ ? { server: truncateIdentity(this.identity.server) }
200
+ : {}),
201
+ ...(this.identity?.file !== undefined
202
+ ? { file: truncateIdentity(this.identity.file) }
203
+ : {}),
204
+ },
145
205
  }),
146
206
  finalPhase,
147
207
  );