@mrclrchtr/supi-lsp 10.0.0 → 11.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 (33) hide show
  1. package/README.md +88 -2
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/LICENSE +21 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/README.md +743 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/index.js +17 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/constants.js +184 -0
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/parse.js +1425 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/picomatch.js +361 -0
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/scan.js +403 -0
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/utils.js +72 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/package.json +83 -0
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/posix.js +3 -0
  12. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +4 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +11 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/src/file-associations.ts +190 -0
  15. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +22 -0
  16. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  17. package/package.json +3 -3
  18. package/src/api.ts +1 -0
  19. package/src/client/client-diagnostic-host.ts +3 -0
  20. package/src/client/client-diagnostics.ts +6 -4
  21. package/src/client/client-request-enrollment.ts +66 -22
  22. package/src/client/client-semantic-input-barrier.ts +37 -5
  23. package/src/client/client.ts +136 -43
  24. package/src/manager/manager.ts +261 -22
  25. package/src/provider/refactor-planning.ts +13 -0
  26. package/src/provider/semantic-edit-discovery.ts +151 -0
  27. package/src/provider/semantic-edit-normalizer.ts +97 -129
  28. package/src/provider/semantic-edit-protocol.ts +132 -0
  29. package/src/session/runtime-controller.ts +8 -1
  30. package/src/session/runtime-registry.ts +7 -20
  31. package/src/session/runtime-rename.ts +172 -0
  32. package/src/session/workspace-lsp-runtime.ts +14 -0
  33. package/src/utils.ts +12 -11
@@ -0,0 +1,190 @@
1
+ import * as path from "node:path";
2
+ import picomatch from "picomatch";
3
+
4
+ /** Language selected for a C-family source file. */
5
+ export type CFamilyLanguage = "c" | "cpp";
6
+
7
+ /** One ordered, project-relative language rule for an existing C-family file. */
8
+ export interface CFamilyFileAssociationRule {
9
+ readonly pattern: string;
10
+ readonly language: CFamilyLanguage;
11
+ }
12
+
13
+ /** C-family suffixes that already have Tree-sitter and LSP support. */
14
+ export const C_FAMILY_EXTENSIONS = Object.freeze([
15
+ ".c",
16
+ ".h",
17
+ ".cpp",
18
+ ".hpp",
19
+ ".cc",
20
+ ".cxx",
21
+ ".hxx",
22
+ ".c++",
23
+ ".h++",
24
+ ] as const);
25
+
26
+ export type CFamilyExtension = (typeof C_FAMILY_EXTENSIONS)[number];
27
+
28
+ const DEFAULT_C_FAMILY_LANGUAGES: Readonly<Record<CFamilyExtension, CFamilyLanguage>> =
29
+ Object.freeze({
30
+ ".c": "c",
31
+ ".h": "c",
32
+ ".cpp": "cpp",
33
+ ".hpp": "cpp",
34
+ ".cc": "cpp",
35
+ ".cxx": "cpp",
36
+ ".hxx": "cpp",
37
+ ".c++": "cpp",
38
+ ".h++": "cpp",
39
+ });
40
+
41
+ /** Immutable C-family language selection for one workspace. */
42
+ export interface CFamilyFileAssociationResolver {
43
+ /** Absolute root used to make project-relative paths. */
44
+ readonly workspaceRoot: string;
45
+ /** Validated ordered rules captured when the workspace host starts. */
46
+ readonly rules: readonly CFamilyFileAssociationRule[];
47
+ /** Select the configured language or the existing suffix default. */
48
+ languageForFile(filePath: string): CFamilyLanguage | undefined;
49
+ }
50
+
51
+ /** Return the current default language for one supported C-family suffix. */
52
+ export function defaultCFamilyLanguage(extension: string): CFamilyLanguage | undefined {
53
+ return DEFAULT_C_FAMILY_LANGUAGES[extension.toLowerCase() as CFamilyExtension];
54
+ }
55
+
56
+ /**
57
+ * Validate and compile ordered C-family file associations for one workspace.
58
+ * Patterns are case-sensitive globs over POSIX-style, workspace-relative paths.
59
+ * Supported patterns use `*`, `?`, `**`, character classes, and brace alternatives.
60
+ * The first matching rule wins. Rules never expand the eligible suffix set.
61
+ *
62
+ * @throws If the rule list, a rule, a language, or a pattern is invalid.
63
+ */
64
+ export function createCFamilyFileAssociationResolver(
65
+ workspaceRoot: string,
66
+ input: unknown = [],
67
+ ): CFamilyFileAssociationResolver {
68
+ const rules = validateRules(input);
69
+ const matchers = rules.map((rule, index) => ({
70
+ language: rule.language,
71
+ matches: compilePattern(rule.pattern, `code-intelligence.fileAssociations[${index}]`),
72
+ }));
73
+ const root = path.resolve(workspaceRoot);
74
+ return Object.freeze({
75
+ workspaceRoot: root,
76
+ rules,
77
+ languageForFile(filePath: string): CFamilyLanguage | undefined {
78
+ const extension = normalizeExtension(filePath);
79
+ const fallback = defaultCFamilyLanguage(extension);
80
+ if (!fallback) return undefined;
81
+
82
+ const absolutePath = path.resolve(root, filePath);
83
+ const relativePath = toPosixPath(path.relative(root, absolutePath));
84
+ if (
85
+ relativePath === ".." ||
86
+ relativePath.startsWith("../") ||
87
+ path.posix.isAbsolute(relativePath)
88
+ ) {
89
+ return fallback;
90
+ }
91
+ for (const rule of matchers) {
92
+ if (rule.matches(relativePath)) return rule.language;
93
+ }
94
+ return fallback;
95
+ },
96
+ });
97
+ }
98
+
99
+ function validateRules(input: unknown): readonly CFamilyFileAssociationRule[] {
100
+ if (!Array.isArray(input)) {
101
+ throw new Error('Invalid "code-intelligence.fileAssociations": expected an ordered array.');
102
+ }
103
+ if (input.length > 64) {
104
+ throw new Error('Invalid "code-intelligence.fileAssociations": use no more than 64 rules.');
105
+ }
106
+ const rules: CFamilyFileAssociationRule[] = [];
107
+ const seenPatterns = new Set<string>();
108
+ for (const [index, value] of input.entries()) {
109
+ const key = `code-intelligence.fileAssociations[${index}]`;
110
+ if (!isRecord(value) || !hasExactKeys(value, ["pattern", "language"])) {
111
+ throw new Error(`${key} must contain only "pattern" and "language" fields.`);
112
+ }
113
+ if (typeof value.pattern !== "string") {
114
+ throw new Error(`${key}.pattern must be a string.`);
115
+ }
116
+ if (value.language !== "c" && value.language !== "cpp") {
117
+ throw new Error(`${key}.language must be "c" or "cpp".`);
118
+ }
119
+ if (seenPatterns.has(value.pattern)) {
120
+ throw new Error(`${key}.pattern duplicates an earlier rule.`);
121
+ }
122
+ seenPatterns.add(value.pattern);
123
+ rules.push(Object.freeze({ pattern: value.pattern, language: value.language }));
124
+ }
125
+ return Object.freeze(rules);
126
+ }
127
+
128
+ function compilePattern(pattern: string, key: string): (file: string) => boolean {
129
+ if (
130
+ pattern.length === 0 ||
131
+ pattern.length > 512 ||
132
+ pattern !== pattern.trim() ||
133
+ pattern.includes("\\") ||
134
+ hasControlCharacter(pattern) ||
135
+ pattern.startsWith("/") ||
136
+ /^[A-Za-z]:/.test(pattern) ||
137
+ pattern.startsWith("!")
138
+ ) {
139
+ throw new Error(`${key}.pattern must be a safe workspace-relative glob using "/" separators.`);
140
+ }
141
+ const segments = pattern.split("/");
142
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
143
+ throw new Error(`${key}.pattern contains an unsafe or unsupported path pattern.`);
144
+ }
145
+ try {
146
+ return picomatch(pattern, {
147
+ nocase: false,
148
+ windows: false,
149
+ dot: true,
150
+ strictBrackets: true,
151
+ nonegate: true,
152
+ noextglob: true,
153
+ maxLength: 512,
154
+ });
155
+ } catch (error) {
156
+ throw new Error(`${key}.pattern is not a valid glob: ${errorMessage(error)}`, { cause: error });
157
+ }
158
+ }
159
+
160
+ function hasControlCharacter(value: string): boolean {
161
+ for (const character of value) {
162
+ const code = character.charCodeAt(0);
163
+ if (code < 32 || code === 127) return true;
164
+ }
165
+ return false;
166
+ }
167
+
168
+ function normalizeExtension(filePath: string): string {
169
+ return path.posix.extname(toPosixPath(filePath)).toLowerCase();
170
+ }
171
+
172
+ function toPosixPath(value: string): string {
173
+ return value.replaceAll("\\", "/");
174
+ }
175
+
176
+ function isRecord(value: unknown): value is Record<string, unknown> {
177
+ return typeof value === "object" && value !== null && !Array.isArray(value);
178
+ }
179
+
180
+ function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
181
+ const allowed = new Set(keys);
182
+ return (
183
+ keys.every((key) => Object.hasOwn(value, key)) &&
184
+ Object.keys(value).every((key) => allowed.has(key))
185
+ );
186
+ }
187
+
188
+ function errorMessage(error: unknown): string {
189
+ return error instanceof Error ? error.message : String(error);
190
+ }
@@ -80,6 +80,14 @@ export interface DocumentCodeSymbol extends CodeSymbol {
80
80
  */
81
81
  export type CodeResult<T> =
82
82
  | { kind: "success"; data: T }
83
+ | {
84
+ kind: "partial";
85
+ data: T;
86
+ file: string;
87
+ reason: string;
88
+ /** Syntax error and missing-node ranges in 0-based UTF-16 coordinates. */
89
+ errorRanges: SourceRange[];
90
+ }
83
91
  | { kind: "unsupported-language"; file: string; message: string }
84
92
  | { kind: "file-access-error"; file: string; message: string }
85
93
  | { kind: "validation-error"; message: string }
@@ -93,7 +101,10 @@ export type ConfidenceMode = "semantic" | "structural" | "heuristic" | "unavaila
93
101
 
94
102
  export interface OutlineData {
95
103
  name: string;
104
+ /** Common structural search category. */
96
105
  kind: string;
106
+ /** Source-language declaration kind, separate from common search category. */
107
+ nativeKind?: string;
97
108
  /** Exact 1-based start of the declaration name token, when known. */
98
109
  nameAnchor?: SymbolAnchor;
99
110
  /** Exclusive 1-based end of a verified literal name span, when known. */
@@ -156,6 +167,12 @@ export interface FileEdit {
156
167
  /** A document-state precondition established when semantic edits are normalized. */
157
168
  export type DocumentEditPrecondition =
158
169
  | { file: string; kind: "open-document-version"; version: number }
170
+ | {
171
+ file: string;
172
+ kind: "open-document-content";
173
+ version: number;
174
+ fingerprint: string;
175
+ }
159
176
  | { file: string; kind: "disk-content" };
160
177
 
161
178
  /**
@@ -257,5 +274,10 @@ export interface CalleesData {
257
274
  /** A single call-site match with name and start line. */
258
275
  export interface CallSite {
259
276
  name: string;
277
+ /** 1-based UTF-16 start position of the written callee expression. */
260
278
  startLine: number;
279
+ startCharacter: number;
280
+ /** Exclusive 1-based UTF-16 end position of the captured source expression. */
281
+ endLine: number;
282
+ endCharacter: number;
261
283
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "10.0.0",
3
+ "version": "11.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-lsp",
3
- "version": "10.0.0",
3
+ "version": "11.0.0",
4
4
  "description": "Language Server Protocol runtime for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  "vscode-jsonrpc": "^9.0.0",
38
38
  "vscode-languageserver-protocol": "^3.18.0",
39
39
  "vscode-languageserver-types": "^3.18.0",
40
- "@mrclrchtr/supi-code-runtime": "10.0.0",
41
- "@mrclrchtr/supi-core": "10.0.0"
40
+ "@mrclrchtr/supi-code-runtime": "11.0.0",
41
+ "@mrclrchtr/supi-core": "11.0.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",
package/src/api.ts CHANGED
@@ -91,6 +91,7 @@ export type {
91
91
  ProcessCrashRecoveryOutcome,
92
92
  ProcessCrashRecoveryReport,
93
93
  RecoverDiagnosticsResult,
94
+ RoutedMutationDocumentSnapshot,
94
95
  RoutedMutationResponse,
95
96
  SemanticReadinessResult,
96
97
  StartupRetryEntry,
@@ -1,3 +1,4 @@
1
+ import type { CFamilyFileAssociationResolver } from "@mrclrchtr/supi-code-runtime/api";
1
2
  import type { DiagnosticRequestAdapter } from "./client-diagnostic-request.ts";
2
3
  import type { NormalizedDocumentSync } from "./client-document-sync.ts";
3
4
 
@@ -7,6 +8,8 @@ export interface ClientDiagnosticsHost {
7
8
  server: string;
8
9
  /** Absolute workspace root, for debug-telemetry identity. */
9
10
  cwd?: string;
11
+ /** Immutable workspace association snapshot used for didOpen document IDs. */
12
+ fileAssociations?: CFamilyFileAssociationResolver;
10
13
  isOperational(): boolean;
11
14
  /** Native pull and server-specific request evidence share this priority adapter. */
12
15
  diagnosticRequestAdapter: DiagnosticRequestAdapter;
@@ -51,6 +51,7 @@ import { SemanticInputRequestRetryGuard } from "./client-request-enrollment.ts";
51
51
  import {
52
52
  SemanticInputBarrier,
53
53
  type SemanticInputDocument,
54
+ type SemanticInputRevision,
54
55
  type SemanticInputSnapshot,
55
56
  type SemanticInputUpdate,
56
57
  } from "./client-semantic-input-barrier.ts";
@@ -156,10 +157,10 @@ export class ClientDiagnostics {
156
157
 
157
158
  /** Fail closed when a semantic request observes a changed input generation. */
158
159
  assertSemanticInputsCurrent(
159
- snapshot: SemanticInputSnapshot,
160
+ snapshot: SemanticInputRevision,
160
161
  control?: CodeRequestControl,
161
162
  contentOverrides?: ReadonlyMap<string, string>,
162
- ): Promise<void> {
163
+ ): Promise<SemanticInputSnapshot> {
163
164
  throwIfCodeRequestInterrupted(control);
164
165
  return this.#inputBarrier.assertCurrent(snapshot, control, contentOverrides);
165
166
  }
@@ -168,6 +169,7 @@ export class ClientDiagnostics {
168
169
  return Array.from(this.#openDocs, ([uri, document]) => ({
169
170
  uri,
170
171
  filePath: uriToFile(uri),
172
+ version: document.version,
171
173
  content: document.content,
172
174
  contentFingerprint: document.contentFingerprint,
173
175
  }));
@@ -293,7 +295,7 @@ export class ClientDiagnostics {
293
295
  this.#cancelDiagnosticRequest(uri);
294
296
  this.#advanceInputRevision(false, false, "enrollment");
295
297
 
296
- const languageId = detectLanguageId(filePath);
298
+ const languageId = detectLanguageId(filePath, this.host.fileAssociations);
297
299
  this.#waiters.cancelSettle();
298
300
  // A didOpen is a synchronization: pushes the server sends after it are
299
301
  // responses to the open and can become fresh evidence (ADR 0020).
@@ -797,7 +799,7 @@ export class ClientDiagnostics {
797
799
 
798
800
  async #verifyDiagnosticInputs(options: {
799
801
  result: CodeQueryResult<Diagnostic[]>;
800
- snapshot: SemanticInputSnapshot;
802
+ snapshot: SemanticInputRevision;
801
803
  control: CodeRequestControl | undefined;
802
804
  contentOverrides: ReadonlyMap<string, string> | undefined;
803
805
  uri: string;
@@ -3,30 +3,61 @@ import {
3
3
  type SemanticInputSynchronizationError,
4
4
  } from "./client-semantic-input-errors.ts";
5
5
 
6
- const MAX_REQUEST_ENROLLMENT_RETRIES = 1;
6
+ const MAX_REQUEST_RETRIES = 1;
7
+ const CONTENT_MODIFIED_ERROR_CODE = -32_801;
7
8
 
8
- /** A request-level enrollment retry decision owned by one request attempt. */
9
- export type RequestEnrollmentRetryDecision =
10
- | { readonly kind: "retry" }
11
- | { readonly kind: "stop"; readonly error: unknown };
9
+ /** Reason that permits one request retry. */
10
+ export type SemanticInputRequestRetryReason = "content-modified" | "enrollment";
11
+
12
+ /** A request-level retry decision owned by one attempt. */
13
+ export type SemanticInputRequestRetryDecision =
14
+ | { readonly kind: "retry"; readonly reason: SemanticInputRequestRetryReason }
15
+ | {
16
+ readonly kind: "stop";
17
+ readonly error: unknown;
18
+ readonly reason?: SemanticInputRequestRetryReason;
19
+ };
12
20
 
13
21
  /** Look up the current input change after a failed request revision. */
14
22
  export type SemanticInputChangeLookup = (
15
23
  revision: number,
16
24
  ) => SemanticInputSynchronizationError | undefined;
17
25
 
26
+ /** Optional request retry policies. ContentModified retries stay off by default. */
27
+ export interface SemanticInputRequestRetryPolicy {
28
+ readonly retryContentModified?: boolean;
29
+ /** Retain a verified revision across document enrollment and a follow-up request. */
30
+ readonly inputRevision?: number;
31
+ }
32
+
33
+ function isContentModifiedError(error: unknown): boolean {
34
+ return (
35
+ typeof error === "object" &&
36
+ error !== null &&
37
+ "code" in error &&
38
+ (error as { code?: unknown }).code === CONTENT_MODIFIED_ERROR_CODE
39
+ );
40
+ }
41
+
18
42
  /**
19
- * Keep one semantic or diagnostic request's enrollment retry policy cohesive.
43
+ * Keep one LSP request's input retry policy in one place.
20
44
  *
21
- * Synchronization passes have their own rejoin budget. This guard only owns
22
- * the request-level budget and the first failed revision used to classify
23
- * later changes.
45
+ * Synchronization passes have their own rejoin budget. This guard owns one
46
+ * shared request retry budget and a protected revision used to classify later
47
+ * changes. A caller can retain an earlier verified revision across enrollment.
48
+ * Protocol retries are disabled unless the caller opts in.
24
49
  */
25
50
  export class SemanticInputRequestRetryGuard {
26
51
  #requestRetries = 0;
27
- #firstFailedRevision: number | undefined;
52
+ #protectedRevision: number | undefined;
53
+ #retryReason: SemanticInputRequestRetryReason | undefined;
28
54
 
29
- constructor(private readonly currentChangeSince: SemanticInputChangeLookup) {}
55
+ constructor(
56
+ private readonly currentChangeSince: SemanticInputChangeLookup,
57
+ private readonly policy: SemanticInputRequestRetryPolicy = {},
58
+ ) {
59
+ this.#protectedRevision = policy.inputRevision;
60
+ }
30
61
 
31
62
  /** Number of request-level retries permitted for this request so far. */
32
63
  get retryCount(): number {
@@ -38,6 +69,11 @@ export class SemanticInputRequestRetryGuard {
38
69
  return this.#requestRetries > 0;
39
70
  }
40
71
 
72
+ /** Cause of the retry already used by this request, if any. */
73
+ get retryReason(): SemanticInputRequestRetryReason | undefined {
74
+ return this.#retryReason;
75
+ }
76
+
41
77
  /** Throw the current non-enrollment cause when it blocks a retry. */
42
78
  assertCurrent(): void {
43
79
  const blockingChange = this.#getBlockingChange();
@@ -45,31 +81,39 @@ export class SemanticInputRequestRetryGuard {
45
81
  }
46
82
 
47
83
  /** Classify one failed attempt and decide whether it may be retried. */
48
- decide(error: unknown, inputRevision: number | undefined): RequestEnrollmentRetryDecision {
49
- if (!isSemanticInputEnrollmentError(error)) {
50
- return { kind: "stop", error };
51
- }
84
+ decide(error: unknown, inputRevision: number | undefined): SemanticInputRequestRetryDecision {
85
+ const reason = this.#getRetryReason(error);
86
+ if (!reason) return { kind: "stop", error };
52
87
 
53
- if (this.#firstFailedRevision !== undefined) {
88
+ if (this.#protectedRevision !== undefined) {
54
89
  const blockingChange = this.#getBlockingChange();
55
90
  if (blockingChange) return { kind: "stop", error: blockingChange };
56
91
  }
57
92
  if (inputRevision === undefined) return { kind: "stop", error };
58
93
 
59
- this.#firstFailedRevision ??= inputRevision;
94
+ this.#protectedRevision ??= inputRevision;
60
95
  const blockingChange = this.#getBlockingChange();
61
96
  if (blockingChange) return { kind: "stop", error: blockingChange };
62
- if (this.#requestRetries >= MAX_REQUEST_ENROLLMENT_RETRIES) {
63
- return { kind: "stop", error };
97
+ if (this.#requestRetries >= MAX_REQUEST_RETRIES) {
98
+ return { kind: "stop", error, reason };
64
99
  }
65
100
 
66
101
  this.#requestRetries++;
67
- return { kind: "retry" };
102
+ this.#retryReason ??= reason;
103
+ return { kind: "retry", reason };
104
+ }
105
+
106
+ #getRetryReason(error: unknown): SemanticInputRequestRetryReason | undefined {
107
+ if (isSemanticInputEnrollmentError(error)) return "enrollment";
108
+ if (this.policy.retryContentModified && isContentModifiedError(error)) {
109
+ return "content-modified";
110
+ }
111
+ return undefined;
68
112
  }
69
113
 
70
114
  #getBlockingChange(): SemanticInputSynchronizationError | undefined {
71
- if (this.#firstFailedRevision === undefined) return undefined;
72
- const currentChange = this.currentChangeSince(this.#firstFailedRevision);
115
+ if (this.#protectedRevision === undefined) return undefined;
116
+ const currentChange = this.currentChangeSince(this.#protectedRevision);
73
117
  return currentChange?.changeKind === "enrollment" ? undefined : currentChange;
74
118
  }
75
119
  }
@@ -21,10 +21,19 @@ const DEFAULT_MAX_CONCURRENT_READS = 16;
21
21
  export interface SemanticInputDocument {
22
22
  readonly uri: string;
23
23
  readonly filePath: string;
24
+ readonly version: number;
24
25
  readonly content: string;
25
26
  readonly contentFingerprint: string;
26
27
  }
27
28
 
29
+ /** One open buffer and disk fingerprint verified by an input synchronization pass. */
30
+ export interface SemanticInputDocumentSnapshot {
31
+ readonly filePath: string;
32
+ readonly version: number;
33
+ readonly contentFingerprint: string;
34
+ readonly diskContentFingerprint: string;
35
+ }
36
+
28
37
  /** One open document update selected by the barrier after a full read. */
29
38
  export interface SemanticInputUpdate {
30
39
  readonly document: SemanticInputDocument;
@@ -48,11 +57,16 @@ export interface SemanticInputBarrierOptions {
48
57
  readonly readFile?: SemanticInputFileReader;
49
58
  }
50
59
 
51
- /** Revision token returned by one completed input synchronization pass. */
52
- export interface SemanticInputSnapshot {
60
+ /** Revision token used to check one semantic input generation. */
61
+ export interface SemanticInputRevision {
53
62
  readonly revision: number;
54
63
  }
55
64
 
65
+ /** Verified inputs returned by one complete synchronization pass. */
66
+ export interface SemanticInputSnapshot extends SemanticInputRevision {
67
+ readonly documents: readonly SemanticInputDocumentSnapshot[];
68
+ }
69
+
56
70
  interface OpenDocumentRead {
57
71
  readonly document: SemanticInputDocument;
58
72
  readonly result:
@@ -150,10 +164,10 @@ export class SemanticInputBarrier {
150
164
 
151
165
  /** Recheck the full input set after a semantic request completes. */
152
166
  async assertCurrent(
153
- snapshot: SemanticInputSnapshot,
167
+ snapshot: SemanticInputRevision,
154
168
  control?: CodeRequestControl,
155
169
  contentOverrides?: ReadonlyMap<string, string>,
156
- ): Promise<void> {
170
+ ): Promise<SemanticInputSnapshot> {
157
171
  if (!this.#host.isOperational()) {
158
172
  throw new Error("Semantic input synchronization is unavailable because the client stopped.");
159
173
  }
@@ -176,6 +190,7 @@ export class SemanticInputBarrier {
176
190
  )
177
191
  );
178
192
  }
193
+ return current;
179
194
  }
180
195
 
181
196
  /** Return the current typed input change after a revision, when present. */
@@ -305,7 +320,24 @@ export class SemanticInputBarrier {
305
320
  // resulting generation as owned by this completed pass so a late caller
306
321
  // can join it instead of starting a redundant read pass.
307
322
  pending.generation = this.#revision;
308
- return { revision: this.#revision };
323
+ const currentDocuments = this.#host.getOpenDocuments();
324
+ const diskFingerprints = new Map(plan.observedDiskFingerprints);
325
+ return {
326
+ revision: this.#revision,
327
+ documents: currentDocuments.flatMap((document) => {
328
+ const diskContentFingerprint = diskFingerprints.get(document.uri);
329
+ return diskContentFingerprint === undefined
330
+ ? []
331
+ : [
332
+ {
333
+ filePath: document.filePath,
334
+ version: document.version,
335
+ contentFingerprint: document.contentFingerprint,
336
+ diskContentFingerprint,
337
+ },
338
+ ];
339
+ }),
340
+ };
309
341
  }
310
342
 
311
343
  #assertPendingCanRun(pending: PendingSynchronization): void {