@mrclrchtr/supi-code-intelligence 5.0.0 → 6.0.1

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 (151) hide show
  1. package/README.md +4 -2
  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-core/README.md +2 -0
  5. package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  6. package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  7. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  8. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/README.md +36 -3
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  20. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -1
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +67 -28
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +63 -6
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +46 -5
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +1 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +278 -79
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +29 -8
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +101 -19
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +19 -1
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +49 -3
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +73 -15
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +1 -1
  33. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +2 -2
  34. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
  35. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
  39. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +4 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
  41. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -3
  42. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +65 -17
  43. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +143 -21
  44. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +2 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -0
  47. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +2 -0
  48. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  49. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  50. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  51. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  52. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  53. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  54. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  55. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  62. package/package.json +5 -5
  63. package/src/analysis/health/diagnostics.ts +58 -126
  64. package/src/analysis/health/file-scope.ts +184 -0
  65. package/src/analysis/health/recovery.ts +8 -1
  66. package/src/analysis/readiness.ts +4 -0
  67. package/src/analysis/search/ast-scan.ts +4 -1
  68. package/src/analysis/search/pattern-analysis.ts +10 -3
  69. package/src/analysis/search/pattern.ts +6 -1
  70. package/src/config.ts +35 -1
  71. package/src/extension.ts +57 -14
  72. package/src/overview/overview-data.ts +9 -2
  73. package/src/overview/overview.ts +49 -30
  74. package/src/overview/types.ts +2 -0
  75. package/src/session/find-types.ts +2 -2
  76. package/src/session/find-workflow.ts +3 -2
  77. package/src/session/health-refresh.ts +76 -49
  78. package/src/session/health-types.ts +7 -1
  79. package/src/session/health-workflow.ts +35 -18
  80. package/src/session/input/workflows.ts +2 -2
  81. package/src/session/inspect-workflow.ts +2 -1
  82. package/src/session/orientation/collect.ts +2 -2
  83. package/src/session/orientation-types.ts +2 -0
  84. package/src/session/orientation-workflow.ts +8 -2
  85. package/src/session/refactor-workflow.ts +2 -1
  86. package/src/session/session.ts +25 -1
  87. package/src/substrate/lsp/maintenance.ts +105 -23
  88. package/src/substrate/lsp/recovery.ts +11 -5
  89. package/src/tool/code_find/execute.ts +23 -0
  90. package/src/tool/code_find/guidance.ts +8 -0
  91. package/src/tool/{find → code_find}/render.ts +1 -1
  92. package/src/tool/{result/find.ts → code_find/result.ts} +36 -11
  93. package/src/tool/code_find/spec.ts +37 -0
  94. package/src/tool/code_graph/execute.ts +24 -0
  95. package/src/tool/code_graph/guidance.ts +6 -0
  96. package/src/tool/{graph → code_graph}/markdown.ts +1 -1
  97. package/src/tool/{result/graph.ts → code_graph/result.ts} +69 -4
  98. package/src/tool/code_graph/spec.ts +39 -0
  99. package/src/tool/{health → code_health}/execute.ts +2 -18
  100. package/src/tool/code_health/guidance.ts +8 -0
  101. package/src/tool/{health → code_health}/markdown.ts +68 -18
  102. package/src/tool/{result/health.ts → code_health/result.ts} +29 -4
  103. package/src/tool/code_health/spec.ts +41 -0
  104. package/src/tool/code_inspect/execute.ts +20 -0
  105. package/src/tool/code_inspect/guidance.ts +6 -0
  106. package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
  107. package/src/tool/{result/inspect.ts → code_inspect/result.ts} +32 -3
  108. package/src/tool/code_inspect/spec.ts +25 -0
  109. package/src/tool/code_orientation/execute.ts +25 -0
  110. package/src/tool/code_orientation/guidance.ts +9 -0
  111. package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
  112. package/src/tool/{result/orientation.ts → code_orientation/result.ts} +67 -3
  113. package/src/tool/code_orientation/spec.ts +25 -0
  114. package/src/tool/code_refactor_apply/execute.ts +17 -0
  115. package/src/tool/code_refactor_apply/guidance.ts +5 -0
  116. package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +5 -11
  117. package/src/tool/code_refactor_apply/spec.ts +26 -0
  118. package/src/tool/code_refactor_plan/execute.ts +27 -0
  119. package/src/tool/code_refactor_plan/guidance.ts +8 -0
  120. package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +9 -22
  121. package/src/tool/code_refactor_plan/spec.ts +25 -0
  122. package/src/tool/code_resolve/execute.ts +19 -0
  123. package/src/tool/code_resolve/guidance.ts +8 -0
  124. package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
  125. package/src/tool/{result/resolve.ts → code_resolve/result.ts} +37 -24
  126. package/src/tool/code_resolve/spec.ts +25 -0
  127. package/src/tool/guidance.ts +71 -52
  128. package/src/tool/infra/truncate.ts +40 -0
  129. package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
  130. package/src/tool/register.ts +9 -97
  131. package/src/tool/result/refactor.ts +1 -4
  132. package/src/tool/schemas.ts +26 -157
  133. package/src/tool/specs.ts +40 -126
  134. package/src/tool/find/execute.ts +0 -43
  135. package/src/tool/graph/execute.ts +0 -82
  136. package/src/tool/inspect/execute.ts +0 -40
  137. package/src/tool/orientation/execute.ts +0 -80
  138. package/src/tool/resolve/execute.ts +0 -53
  139. /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
  140. /package/src/tool/{find → code_find}/markdown.ts +0 -0
  141. /package/src/tool/{find → code_find}/modes.ts +0 -0
  142. /package/src/tool/{find → code_find}/tui.ts +0 -0
  143. /package/src/tool/{graph → code_graph}/tui.ts +0 -0
  144. /package/src/tool/{health → code_health}/refresh-status.ts +0 -0
  145. /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
  146. /package/src/tool/{health → code_health}/tui.ts +0 -0
  147. /package/src/tool/{inspect → code_inspect}/tui.ts +0 -0
  148. /package/src/tool/{orientation → code_orientation}/tui.ts +0 -0
  149. /package/src/tool/{refactor-apply → code_refactor_apply}/tui.ts +0 -0
  150. /package/src/tool/{refactor-plan → code_refactor_plan}/tui.ts +0 -0
  151. /package/src/tool/{resolve → code_resolve}/tui.ts +0 -0
@@ -163,6 +163,25 @@ export interface DiagnosticCacheEntry {
163
163
  evidenceRevision?: number;
164
164
  version?: number;
165
165
  resultId?: string;
166
+ /**
167
+ * Valid publications observed for the entry's synchronization. The first
168
+ * valid publication for a synchronization is tentative; a later valid
169
+ * publication promotes the entry to confirmed (ADR 0021). A confirmed
170
+ * pull entry counts as the first publication when a push continues the
171
+ * same synchronization, so that push keeps the confirmation. Pull
172
+ * entries as such carry no count and are confirmed on arrival.
173
+ */
174
+ publications?: number;
175
+ }
176
+
177
+ /**
178
+ * Test whether one push entry is still tentative for its synchronization.
179
+ *
180
+ * A pull entry confirms on arrival; a push entry confirms only after a
181
+ * later valid publication for the same synchronization (ADR 0021).
182
+ */
183
+ export function isTentativePushEntry(entry: DiagnosticCacheEntry | undefined): boolean {
184
+ return Boolean(entry && entry.source === "push" && (entry.publications ?? 1) < 2);
166
185
  }
167
186
 
168
187
  /** One document synchronization that needs current diagnostic evidence. */
@@ -204,6 +223,7 @@ export function hasFreshPush(
204
223
  const entry = store.get(synchronization.uri);
205
224
  return Boolean(
206
225
  entry?.source === "push" &&
226
+ !isTentativePushEntry(entry) &&
207
227
  entry.synchronizationId === synchronization.synchronizationId &&
208
228
  (synchronization.evidenceRevision === undefined ||
209
229
  entry.evidenceRevision === synchronization.evidenceRevision) &&
@@ -211,8 +231,11 @@ export function hasFreshPush(
211
231
  );
212
232
  }
213
233
 
214
- /** Test whether pull or push evidence confirms the supplied synchronization. */
215
- export function hasFreshEvidence(
234
+ /**
235
+ * Test whether stored evidence matches a synchronization without a
236
+ * confirmation gate. A tentative push matches here but cannot confirm.
237
+ */
238
+ export function hasCurrentEvidence(
216
239
  store: ReadonlyMap<string, DiagnosticCacheEntry>,
217
240
  synchronization: DiagnosticSynchronization,
218
241
  currentEvidenceRevision?: number,
@@ -227,8 +250,26 @@ export function hasFreshEvidence(
227
250
  );
228
251
  }
229
252
 
230
- /** Return the latest cache update that confirms one of the synchronizations. */
231
- export function latestFreshEvidenceReceivedAt(
253
+ /** Test whether pull or push evidence confirms the supplied synchronization. */
254
+ export function hasFreshEvidence(
255
+ store: ReadonlyMap<string, DiagnosticCacheEntry>,
256
+ synchronization: DiagnosticSynchronization,
257
+ currentEvidenceRevision?: number,
258
+ ): boolean {
259
+ return (
260
+ hasCurrentEvidence(store, synchronization, currentEvidenceRevision) &&
261
+ !isTentativePushEntry(store.get(synchronization.uri))
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Return the latest cache update that matches one of the synchronizations.
267
+ *
268
+ * Every accepted publication restarts the settle quiet period, tentative
269
+ * publications included, so this helper does not apply the confirmation
270
+ * gate (ADR 0021).
271
+ */
272
+ export function latestCurrentEvidenceReceivedAt(
232
273
  store: ReadonlyMap<string, DiagnosticCacheEntry>,
233
274
  synchronizations: DiagnosticSynchronization[],
234
275
  currentEvidenceRevision?: number,
@@ -236,7 +277,7 @@ export function latestFreshEvidenceReceivedAt(
236
277
  let latest = 0;
237
278
  for (const synchronization of synchronizations) {
238
279
  const entry = store.get(synchronization.uri);
239
- if (hasFreshEvidence(store, synchronization, currentEvidenceRevision)) {
280
+ if (hasCurrentEvidence(store, synchronization, currentEvidenceRevision)) {
240
281
  latest = Math.max(latest, entry?.receivedAt ?? 0);
241
282
  }
242
283
  }
@@ -9,6 +9,7 @@ export interface ClientDiagnosticsHost {
9
9
  cwd?: string;
10
10
  isOperational(): boolean;
11
11
  supportsPullDiagnostics(): boolean;
12
+ usesIncrementalDocumentSync(): boolean;
12
13
  sendNotification(method: string, params: unknown): void;
13
14
  pullDocumentDiagnostics(request: DiagnosticPullRequest): Promise<DocumentDiagnosticReport | null>;
14
15
  }
@@ -0,0 +1,214 @@
1
+ // Bounded push-publication telemetry for one client's diagnostic state.
2
+ //
3
+ // Push-only servers can publish an early result and a later semantic result
4
+ // for one document synchronization (ADR 0021). This tracker records one
5
+ // bounded per-synchronization publication summary per diagnostic operation
6
+ // and one ambient event when a later publication promotes a synchronization
7
+ // that an earlier operation already reported unconfirmed. Events carry only
8
+ // bounded server, workspace, relative-file, synchronization identity, count,
9
+ // and timing data. They never carry diagnostic payloads or source text.
10
+
11
+ import * as path from "node:path";
12
+ import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
13
+ import { boundCwd, truncateIdentity } from "../debug-telemetry.ts";
14
+ import { uriToFile } from "../utils.ts";
15
+
16
+ /** Maximum tracked synchronizations before the oldest entry is evicted. */
17
+ export const MAX_TRACKED_SYNCHRONIZATIONS = 64;
18
+
19
+ /** Maximum synchronization entries in one bounded publication summary. */
20
+ export const MAX_SUMMARY_SYNCHRONIZATIONS = 16;
21
+
22
+ /** Maximum publication count retained for telemetry; 2 means two or more. */
23
+ const MAX_PUBLICATIONS = 2;
24
+
25
+ /** Bounded identity for one publication telemetry observation. */
26
+ export interface DiagnosticPublicationIdentity {
27
+ /** Configured server name. */
28
+ readonly server?: string;
29
+ /** Absolute workspace root. */
30
+ readonly cwd?: string;
31
+ /** Workspace-relative file path; sync-file operations only. */
32
+ readonly file?: string;
33
+ }
34
+
35
+ /** One awaited synchronization of a completed diagnostic operation. */
36
+ export interface DiagnosticPublicationSynchronization {
37
+ readonly uri: string;
38
+ readonly synchronizationId: number;
39
+ readonly evidenceRevision: number;
40
+ readonly confirmed: boolean;
41
+ }
42
+
43
+ /** One bounded per-synchronization publication summary entry. */
44
+ export interface DiagnosticPublicationSummaryEntry {
45
+ readonly synchronizationId: number;
46
+ readonly publications: number;
47
+ readonly firstReceivedAt: number;
48
+ readonly lastReceivedAt: number;
49
+ readonly confirmed: boolean;
50
+ }
51
+
52
+ interface SynchronizationPublicationState {
53
+ readonly uri: string;
54
+ readonly synchronizationId: number;
55
+ readonly evidenceRevision: number;
56
+ publications: number;
57
+ firstReceivedAt: number;
58
+ lastReceivedAt: number;
59
+ /** When a finished operation first reported this synchronization unconfirmed. */
60
+ unconfirmedAt?: number;
61
+ }
62
+
63
+ function synchronizationKey(
64
+ uri: string,
65
+ synchronizationId: number,
66
+ evidenceRevision: number,
67
+ ): string {
68
+ return `${uri}|${synchronizationId}|${evidenceRevision}`;
69
+ }
70
+
71
+ /** Track bounded push-publication counts for one LSP client. */
72
+ export class DiagnosticPublicationTracker {
73
+ readonly #states = new Map<string, SynchronizationPublicationState>();
74
+
75
+ constructor(
76
+ private readonly identity: { server?: string; cwd?: string },
77
+ private readonly fileFor: (uri: string) => string | undefined,
78
+ ) {}
79
+
80
+ /**
81
+ * Record one accepted push publication for a synchronization.
82
+ *
83
+ * The state map is bounded to {@link MAX_TRACKED_SYNCHRONIZATIONS}; the
84
+ * oldest tracked synchronization is evicted when the bound is exceeded.
85
+ */
86
+ record(
87
+ uri: string,
88
+ synchronizationId: number,
89
+ evidenceRevision: number,
90
+ receivedAt: number = Date.now(),
91
+ ): void {
92
+ const key = synchronizationKey(uri, synchronizationId, evidenceRevision);
93
+ const existing = this.#states.get(key);
94
+ if (existing) {
95
+ existing.publications = Math.min(existing.publications + 1, MAX_PUBLICATIONS);
96
+ existing.lastReceivedAt = receivedAt;
97
+ return;
98
+ }
99
+ this.#states.set(key, {
100
+ uri,
101
+ synchronizationId,
102
+ evidenceRevision,
103
+ publications: 1,
104
+ firstReceivedAt: receivedAt,
105
+ lastReceivedAt: receivedAt,
106
+ });
107
+ if (this.#states.size > MAX_TRACKED_SYNCHRONIZATIONS) {
108
+ const oldest = this.#states.keys().next().value;
109
+ if (oldest !== undefined) this.#states.delete(oldest);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Emit one bounded per-synchronization publication summary.
115
+ *
116
+ * Synchronizations with no observed push publication are omitted. A
117
+ * synchronization that ends unconfirmed is marked so a later promotion can
118
+ * emit the ambient late-republish event.
119
+ */
120
+ emitSummary(options: {
121
+ readonly operation: "refresh-open" | "sync-file";
122
+ readonly identity: DiagnosticPublicationIdentity;
123
+ readonly synchronizations: readonly DiagnosticPublicationSynchronization[];
124
+ readonly operationId?: string;
125
+ }): void {
126
+ const entries: DiagnosticPublicationSummaryEntry[] = [];
127
+ const now = Date.now();
128
+ for (const synchronization of options.synchronizations) {
129
+ const state = this.#states.get(
130
+ synchronizationKey(
131
+ synchronization.uri,
132
+ synchronization.synchronizationId,
133
+ synchronization.evidenceRevision,
134
+ ),
135
+ );
136
+ if (!state) continue;
137
+ entries.push({
138
+ synchronizationId: state.synchronizationId,
139
+ publications: state.publications,
140
+ firstReceivedAt: state.firstReceivedAt,
141
+ lastReceivedAt: state.lastReceivedAt,
142
+ confirmed: synchronization.confirmed,
143
+ });
144
+ if (!synchronization.confirmed) state.unconfirmedAt ??= now;
145
+ else state.unconfirmedAt = undefined;
146
+ }
147
+ if (entries.length === 0) return;
148
+ recordDebugEvent({
149
+ operationId: options.operationId,
150
+ source: "lsp",
151
+ level: "debug",
152
+ category: "diagnostics.publication",
153
+ message: "LSP diagnostic publication summary",
154
+ cwd: boundCwd(options.identity.cwd),
155
+ data: {
156
+ operation: options.operation,
157
+ synchronizations: entries.slice(0, MAX_SUMMARY_SYNCHRONIZATIONS),
158
+ ...(options.identity.server !== undefined
159
+ ? { server: truncateIdentity(options.identity.server) }
160
+ : {}),
161
+ ...(options.identity.file !== undefined
162
+ ? { file: truncateIdentity(options.identity.file) }
163
+ : {}),
164
+ },
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Observe one promotion of a synchronization by a later publication.
170
+ *
171
+ * The ambient late-republish event fires only when a finished operation
172
+ * previously reported the synchronization unconfirmed. The mark is cleared
173
+ * so one promotion emits at most one event.
174
+ */
175
+ promoted(
176
+ uri: string,
177
+ synchronizationId: number,
178
+ evidenceRevision: number,
179
+ receivedAt: number = Date.now(),
180
+ ): void {
181
+ const state = this.#states.get(synchronizationKey(uri, synchronizationId, evidenceRevision));
182
+ const unconfirmedAt = state?.unconfirmedAt;
183
+ if (!state || state.publications < 2 || unconfirmedAt === undefined) return;
184
+ state.unconfirmedAt = undefined;
185
+ const file = this.fileFor(uri);
186
+ recordDebugEvent({
187
+ source: "lsp",
188
+ level: "debug",
189
+ category: "diagnostics.publication",
190
+ message: "LSP diagnostic late republish",
191
+ cwd: boundCwd(this.identity.cwd),
192
+ data: {
193
+ synchronizationId,
194
+ publications: state.publications,
195
+ receivedAt,
196
+ delayMs: Math.max(0, receivedAt - unconfirmedAt),
197
+ ...(this.identity.server !== undefined
198
+ ? { server: truncateIdentity(this.identity.server) }
199
+ : {}),
200
+ ...(file !== undefined ? { file: truncateIdentity(file) } : {}),
201
+ },
202
+ });
203
+ }
204
+ }
205
+
206
+ /** Return a workspace-relative diagnostic file path for telemetry identity. */
207
+ export function trackerFileIdentityFor(
208
+ cwd: string | undefined,
209
+ ): (uri: string) => string | undefined {
210
+ return (uri) => {
211
+ if (cwd === undefined) return undefined;
212
+ return path.relative(cwd, path.resolve(cwd, uriToFile(uri)));
213
+ };
214
+ }