ast-mcp-server 0.6.0 → 0.7.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 (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +47 -15
  3. package/SECURITY.md +31 -0
  4. package/dist/cli.js +0 -0
  5. package/dist/index.js +129 -10
  6. package/dist/server.js +21 -2
  7. package/dist/services/agent-setup.js +36 -117
  8. package/dist/services/agent-targets.js +122 -0
  9. package/dist/services/context-builder.js +52 -24
  10. package/dist/services/diagnostics.js +10 -5
  11. package/dist/services/file-fingerprints.js +104 -0
  12. package/dist/services/file-snapshot.js +8 -1
  13. package/dist/services/impact.js +334 -0
  14. package/dist/services/operations.js +343 -254
  15. package/dist/services/outline.js +94 -21
  16. package/dist/services/pagination.js +10 -4
  17. package/dist/services/project-operation-scheduler.js +475 -0
  18. package/dist/services/project-status.js +134 -52
  19. package/dist/services/project-watcher.js +230 -0
  20. package/dist/services/project.js +646 -66
  21. package/dist/services/public-errors.js +255 -0
  22. package/dist/services/references.js +7 -1
  23. package/dist/services/relationships.js +1198 -0
  24. package/dist/services/request-context.js +42 -0
  25. package/dist/services/runtime-activity.js +65 -0
  26. package/dist/services/runtime-logger.js +50 -0
  27. package/dist/services/runtime-policy.js +47 -0
  28. package/dist/services/setup-wizard.js +4 -7
  29. package/dist/services/shutdown.js +93 -0
  30. package/dist/services/symbol-index-limits.js +5 -0
  31. package/dist/services/symbol-index-order.js +13 -0
  32. package/dist/services/symbol-index-policy.js +91 -0
  33. package/dist/services/symbol-index-sqlite.js +850 -0
  34. package/dist/services/symbol-index.js +447 -0
  35. package/dist/services/symbols.js +403 -79
  36. package/dist/services/test-candidates.js +209 -0
  37. package/dist/services/workspace.js +18 -7
  38. package/dist/tools/apply_operation.js +8 -4
  39. package/dist/tools/explore.js +8 -6
  40. package/dist/tools/find_references.js +12 -13
  41. package/dist/tools/get_diagnostics.js +10 -7
  42. package/dist/tools/get_file.js +23 -11
  43. package/dist/tools/get_impact.js +134 -0
  44. package/dist/tools/get_operation_preview.js +7 -4
  45. package/dist/tools/get_outline.js +8 -6
  46. package/dist/tools/get_project_status.js +77 -8
  47. package/dist/tools/get_symbol_source.js +8 -6
  48. package/dist/tools/list_files.js +11 -6
  49. package/dist/tools/rename_symbol.js +6 -4
  50. package/dist/tools/replace_symbol_body.js +6 -4
  51. package/dist/tools/result.js +27 -17
  52. package/dist/tools/scaffold_class.js +6 -4
  53. package/dist/tools/search_symbols.js +15 -14
  54. package/docs/support.md +80 -0
  55. package/package.json +16 -5
  56. package/skills/structural-code-editing/SKILL.md +37 -10
@@ -0,0 +1,122 @@
1
+ export const MCP_SERVER_NAME = "ast";
2
+ const EXPECTED_HERMES_TOOLS = [
3
+ "ast_list_files",
4
+ "ast_get_outline",
5
+ "ast_get_symbol_source",
6
+ "ast_search_symbols",
7
+ "ast_find_references",
8
+ "ast_get_diagnostics",
9
+ "ast_rename_symbol",
10
+ "ast_replace_symbol_body",
11
+ "ast_scaffold_class",
12
+ "ast_get_operation_preview",
13
+ "ast_apply_operation",
14
+ ];
15
+ function isMissingClaudeMcp(output) {
16
+ return (/\bno\s+mcp\s+server\b/i.test(output) &&
17
+ /(?:\bfound\b|\bnamed?\b)/i.test(output) &&
18
+ /\bast\b/i.test(output));
19
+ }
20
+ function inspectClaudeFields(stdout) {
21
+ const fields = new Map();
22
+ for (const line of stdout.split(/\r?\n/)) {
23
+ const match = line.match(/^\s*(Status|Type|Command|Args):\s*(.*)$/i);
24
+ if (match !== null)
25
+ fields.set(match[1].toLowerCase(), match[2].trim());
26
+ }
27
+ return fields;
28
+ }
29
+ const claudeMcp = {
30
+ async inspect(runtime, context) {
31
+ const result = await runtime.run(["mcp", "get", MCP_SERVER_NAME]);
32
+ const output = `${result.stdout}\n${result.stderr}`;
33
+ if (result.exitCode !== 0) {
34
+ return isMissingClaudeMcp(output)
35
+ ? { status: "missing" }
36
+ : { status: "error", operation: "MCP inspection", result };
37
+ }
38
+ const fields = inspectClaudeFields(result.stdout);
39
+ const current = /connected/i.test(fields.get("status") ?? "") &&
40
+ fields.get("type")?.toLowerCase() === "stdio" &&
41
+ fields.get("command") === context.nodeExecutable &&
42
+ fields.get("args") === context.serverEntryPath;
43
+ return current
44
+ ? { status: "current" }
45
+ : {
46
+ status: "conflict",
47
+ detail: "The existing user-scoped 'ast' registration does not match this package.",
48
+ };
49
+ },
50
+ register(runtime, context) {
51
+ return runtime.run([
52
+ "mcp",
53
+ "add",
54
+ "--scope",
55
+ "user",
56
+ "--transport",
57
+ "stdio",
58
+ MCP_SERVER_NAME,
59
+ "--",
60
+ context.nodeExecutable,
61
+ context.serverEntryPath,
62
+ ]);
63
+ },
64
+ registrationAccepted(result) {
65
+ return result.exitCode === 0;
66
+ },
67
+ };
68
+ const hermesMcp = {
69
+ async inspect(runtime) {
70
+ const listed = await runtime.run(["mcp", "list"]);
71
+ if (listed.exitCode !== 0) {
72
+ return { status: "error", operation: "MCP list", result: listed };
73
+ }
74
+ if (!/^\s*ast(?:\s|$)/im.test(listed.stdout))
75
+ return { status: "missing" };
76
+ const tested = await runtime.run(["mcp", "test", MCP_SERVER_NAME]);
77
+ const current = tested.exitCode === 0 && EXPECTED_HERMES_TOOLS.every((tool) => tested.stdout.includes(tool));
78
+ return current
79
+ ? { status: "current" }
80
+ : {
81
+ status: "conflict",
82
+ detail: "The existing 'ast' registration does not expose the expected structural tools.",
83
+ };
84
+ },
85
+ register(runtime, context) {
86
+ return runtime.run([
87
+ "mcp",
88
+ "add",
89
+ MCP_SERVER_NAME,
90
+ "--command",
91
+ context.nodeExecutable,
92
+ "--args",
93
+ context.serverEntryPath,
94
+ ], "\n");
95
+ },
96
+ registrationAccepted(result) {
97
+ return result.exitCode === 0 && /saved\s+'ast'/i.test(result.stdout);
98
+ },
99
+ };
100
+ export const AGENT_TARGETS = [
101
+ {
102
+ id: "claude",
103
+ label: "Claude Code",
104
+ command: "claude",
105
+ skillTarget: "claude",
106
+ mcp: claudeMcp,
107
+ },
108
+ {
109
+ id: "hermes",
110
+ label: "Hermes",
111
+ command: "hermes",
112
+ skillTarget: "hermes",
113
+ mcp: hermesMcp,
114
+ },
115
+ ];
116
+ export const AGENT_IDS = AGENT_TARGETS.map((target) => target.id);
117
+ export function getAgentTarget(id) {
118
+ const target = AGENT_TARGETS.find((candidate) => candidate.id === id);
119
+ if (target === undefined)
120
+ throw new Error(`Unsupported agent target: ${id}`);
121
+ return target;
122
+ }
@@ -2,9 +2,10 @@ import { Buffer } from "node:buffer";
2
2
  import path from "node:path";
3
3
  import { buildFileOutline, nodeSourceWithLocation } from "./outline.js";
4
4
  import { paginate } from "./pagination.js";
5
- import { findDeclarationByName, getSourceFileOrThrow } from "./project.js";
5
+ import { isCooperativeInterruption, NO_REQUEST_CONTEXT, } from "./request-context.js";
6
+ import { findDeclarationByName, getSourceFileOrThrow, reportSymbolIndexFailure, } from "./project.js";
6
7
  import { collectSymbolReferences } from "./references.js";
7
- import { searchProjectSymbols, sourceFileSymbols, symbolMatchRank, } from "./symbols.js";
8
+ import { searchProjectSymbolsPage, searchProjectSymbolsPageWithIndex, sourceFileSymbols, symbolMatchRank, } from "./symbols.js";
8
9
  export const EXPLORE_DEFAULT_LIMIT = 20;
9
10
  export const EXPLORE_DEFAULT_REFERENCE_LIMIT = 20;
10
11
  export const EXPLORE_DEFAULT_MAX_BYTES = 64 * 1024;
@@ -48,14 +49,14 @@ function recordFromOutlineSymbol(sourceFile, projectRoot, symbol) {
48
49
  signature: symbol.signature,
49
50
  };
50
51
  }
51
- function fileRecords(sourceFile, projectRoot) {
52
- const outline = buildFileOutline(sourceFile);
52
+ function fileRecords(sourceFile, projectRoot, requestContext) {
53
+ const outline = buildFileOutline(sourceFile, requestContext);
53
54
  if (outline.symbols.length > 0) {
54
55
  return outline.symbols.map((symbol) => recordFromOutlineSymbol(sourceFile, projectRoot, symbol));
55
56
  }
56
- return sourceFileSymbols(sourceFile, projectRoot);
57
+ return sourceFileSymbols(sourceFile, projectRoot, requestContext);
57
58
  }
58
- function projectRecords(context, request) {
59
+ async function projectRecords(context, request, requestContext) {
59
60
  const { project, projectRoot } = context;
60
61
  if (request.filePath) {
61
62
  const sourceFile = getSourceFileOrThrow(project, request.filePath);
@@ -63,7 +64,7 @@ function projectRecords(context, request) {
63
64
  const requestedSymbolPath = request.symbolPath;
64
65
  if (requestedSymbolPath) {
65
66
  const node = findDeclarationByName(sourceFile, requestedSymbolPath);
66
- const record = fileRecords(sourceFile, projectRoot).find((candidate) => candidate.symbol_path === requestedSymbolPath ||
67
+ const record = fileRecords(sourceFile, projectRoot, requestContext).find((candidate) => candidate.symbol_path === requestedSymbolPath ||
67
68
  candidate.selector === requestedSymbolPath ||
68
69
  (candidate.symbol_path === requestedSymbolPath.split("@")[0] &&
69
70
  candidate.line === node.getStartLineNumber()));
@@ -71,29 +72,47 @@ function projectRecords(context, request) {
71
72
  throw new Error(`Symbol "${requestedSymbolPath}" could not be projected from ${file}.`);
72
73
  }
73
74
  return {
75
+ context,
74
76
  route: "symbol",
75
77
  file,
76
78
  symbol: record.selector,
77
79
  records: [record],
78
80
  };
79
81
  }
80
- const records = fileRecords(sourceFile, projectRoot)
82
+ const records = fileRecords(sourceFile, projectRoot, requestContext)
81
83
  .filter((record) => !request.query || matchesQuery(record, request.query))
82
84
  .filter((record) => !request.kinds || request.kinds.includes(record.kind));
83
- return { route: "file", file, symbol: null, records: sortRecords(records, request.query) };
85
+ return {
86
+ context,
87
+ route: "file",
88
+ file,
89
+ symbol: null,
90
+ records: sortRecords(records, request.query),
91
+ };
84
92
  }
85
93
  if (!request.query) {
86
94
  throw new Error("ast_explore requires query, file_path, or both file_path and symbol_path.");
87
95
  }
96
+ let effectiveContext = context;
97
+ const indexedPage = await searchProjectSymbolsPageWithIndex(project, projectRoot, context.status.project, context.symbolIndex, context.symbolIndexReady, {
98
+ query: request.query,
99
+ kinds: request.kinds,
100
+ fileFilter: request.fileFilter,
101
+ }, request.offset, request.limit, async (reason) => {
102
+ effectiveContext = (await reportSymbolIndexFailure(projectRoot, reason)) ?? effectiveContext;
103
+ }, requestContext);
88
104
  return {
105
+ context: effectiveContext,
89
106
  route: "query",
90
107
  file: null,
91
108
  symbol: null,
92
- records: searchProjectSymbols(project, projectRoot, {
93
- query: request.query,
94
- kinds: request.kinds,
95
- fileFilter: request.fileFilter,
96
- }),
109
+ page: indexedPage ??
110
+ searchProjectSymbolsPage(effectiveContext.project, effectiveContext.projectRoot, {
111
+ query: request.query,
112
+ kinds: request.kinds,
113
+ fileFilter: request.fileFilter,
114
+ }, request.offset, request.limit, requestContext),
115
+ records: [],
97
116
  };
98
117
  }
99
118
  function projectSymbol(detail, record) {
@@ -120,16 +139,20 @@ function effectiveExpansion(request) {
120
139
  references: request.includeReferences ?? request.detail === "full",
121
140
  };
122
141
  }
123
- export function buildExploreContext(context, request) {
124
- const routed = projectRecords(context, request);
125
- const page = paginate(routed.records, request.offset, request.limit);
142
+ export async function buildExploreContext(context, request, requestContext = NO_REQUEST_CONTEXT) {
143
+ requestContext.checkpoint();
144
+ const routed = await projectRecords(context, request, requestContext);
145
+ requestContext.checkpoint();
146
+ const effectiveContext = routed.context;
147
+ const page = routed.page ?? paginate(routed.records, request.offset, request.limit);
126
148
  const expansion = effectiveExpansion(request);
127
149
  const unresolved = [];
128
150
  const evidence = [];
129
151
  for (const record of page.items) {
152
+ requestContext.checkpoint();
130
153
  let sourceFile;
131
154
  try {
132
- sourceFile = getSourceFileOrThrow(context.project, record.file);
155
+ sourceFile = getSourceFileOrThrow(effectiveContext.project, record.file);
133
156
  }
134
157
  catch {
135
158
  unresolved.push({ selector: record.selector, reason: "source_unresolved" });
@@ -145,13 +168,16 @@ export function buildExploreContext(context, request) {
145
168
  }
146
169
  let source;
147
170
  let references;
148
- if (expansion.source)
149
- source = nodeSourceWithLocation(node, context.projectRoot);
171
+ if (expansion.source) {
172
+ source = nodeSourceWithLocation(node, effectiveContext.projectRoot, requestContext);
173
+ }
150
174
  if (expansion.references) {
151
175
  try {
152
- references = collectSymbolReferences(node, context.projectRoot, record.selector, true, request.referenceDetail, 0, request.referenceLimit);
176
+ references = collectSymbolReferences(node, effectiveContext.projectRoot, record.selector, true, request.referenceDetail, 0, request.referenceLimit, requestContext);
153
177
  }
154
- catch {
178
+ catch (error) {
179
+ if (isCooperativeInterruption(error))
180
+ throw error;
155
181
  unresolved.push({ selector: record.selector, reason: "references_unresolved" });
156
182
  }
157
183
  }
@@ -167,9 +193,10 @@ export function buildExploreContext(context, request) {
167
193
  let returnedSymbols = [...symbols];
168
194
  let returnedEvidence = [...evidence];
169
195
  let reason = page.has_more ? "record_limit" : null;
170
- let result = createResult(context, request, routed.route, routed.file, routed.symbol, returnedSymbols, returnedEvidence, page.total, unresolved, reason);
196
+ let result = createResult(effectiveContext, request, routed.route, routed.file, routed.symbol, returnedSymbols, returnedEvidence, page.total, unresolved, reason);
171
197
  while (jsonBytes(result) > request.maxBytes &&
172
198
  (returnedEvidence.length > 0 || returnedSymbols.length > 0)) {
199
+ requestContext.checkpoint();
173
200
  reason = "byte_limit";
174
201
  if (returnedEvidence.length > 0) {
175
202
  returnedEvidence = returnedEvidence.slice(0, -1);
@@ -179,8 +206,9 @@ export function buildExploreContext(context, request) {
179
206
  const selectors = new Set(returnedSymbols.map((symbol) => symbol.selector));
180
207
  returnedEvidence = returnedEvidence.filter((item) => selectors.has(item.selector));
181
208
  }
182
- result = createResult(context, request, routed.route, routed.file, routed.symbol, returnedSymbols, returnedEvidence, page.total, unresolved, reason);
209
+ result = createResult(effectiveContext, request, routed.route, routed.file, routed.symbol, returnedSymbols, returnedEvidence, page.total, unresolved, reason);
183
210
  }
211
+ requestContext.checkpoint();
184
212
  const usedBytes = jsonBytes(result);
185
213
  return {
186
214
  ...result,
@@ -1,6 +1,8 @@
1
1
  import path from "node:path";
2
2
  import { DiagnosticCategory, ts } from "ts-morph";
3
- export function normalizeDiagnostic(diagnostic, projectRoot) {
3
+ import { NO_REQUEST_CONTEXT } from "./request-context.js";
4
+ export function normalizeDiagnostic(diagnostic, projectRoot, requestContext = NO_REQUEST_CONTEXT) {
5
+ requestContext.checkpoint();
4
6
  const sourceFile = diagnostic.getSourceFile();
5
7
  const start = diagnostic.getStart();
6
8
  const location = sourceFile && start !== undefined ? sourceFile.getLineAndColumnAtPos(start) : undefined;
@@ -21,14 +23,16 @@ function identity(diagnostic) {
21
23
  diagnostic.message,
22
24
  ]);
23
25
  }
24
- function subtract(candidates, baseline) {
26
+ function subtract(candidates, baseline, requestContext) {
25
27
  const counts = new Map();
26
28
  for (const diagnostic of baseline) {
29
+ requestContext.checkpoint();
27
30
  const key = identity(diagnostic);
28
31
  counts.set(key, (counts.get(key) ?? 0) + 1);
29
32
  }
30
33
  const difference = [];
31
34
  for (const diagnostic of candidates) {
35
+ requestContext.checkpoint();
32
36
  const key = identity(diagnostic);
33
37
  const remaining = counts.get(key) ?? 0;
34
38
  if (remaining > 0) {
@@ -40,9 +44,10 @@ function subtract(candidates, baseline) {
40
44
  }
41
45
  return difference;
42
46
  }
43
- export function compareDiagnostics(before, after) {
44
- const added = subtract(after, before);
45
- const removed = subtract(before, after);
47
+ export function compareDiagnostics(before, after, requestContext = NO_REQUEST_CONTEXT) {
48
+ requestContext.checkpoint();
49
+ const added = subtract(after, before, requestContext);
50
+ const removed = subtract(before, after, requestContext);
46
51
  return {
47
52
  added,
48
53
  removed,
@@ -0,0 +1,104 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ function isMissingFileError(error) {
5
+ return (typeof error === "object" &&
6
+ error !== null &&
7
+ "code" in error &&
8
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
9
+ }
10
+ function resolveCandidatePath(filePath) {
11
+ try {
12
+ return fs.realpathSync(filePath);
13
+ }
14
+ catch (error) {
15
+ if (!isMissingFileError(error))
16
+ throw error;
17
+ return path.resolve(filePath);
18
+ }
19
+ }
20
+ function assertRegularFile(filePath) {
21
+ const stats = fs.statSync(filePath, { bigint: true });
22
+ if (!stats.isFile()) {
23
+ throw new Error(`Cannot fingerprint non-file path: ${filePath}`);
24
+ }
25
+ return stats;
26
+ }
27
+ function metadataMatches(previous, current) {
28
+ return (previous.size_bytes === current.size_bytes &&
29
+ previous.mtime_ns === current.mtime_ns &&
30
+ previous.device === current.device &&
31
+ previous.inode === current.inode);
32
+ }
33
+ export function hashBytes(content) {
34
+ return createHash("sha256").update(content).digest("hex");
35
+ }
36
+ export function captureFileFingerprint(filePath, previous, options = {}) {
37
+ const canonicalPath = fs.realpathSync(filePath);
38
+ const stats = assertRegularFile(canonicalPath);
39
+ const metadata = {
40
+ size_bytes: Number(stats.size),
41
+ mtime_ns: stats.mtimeNs.toString(),
42
+ device: stats.dev.toString(),
43
+ inode: stats.ino.toString(),
44
+ };
45
+ if (!Number.isSafeInteger(metadata.size_bytes)) {
46
+ throw new Error(`File size is outside the safe integer range: ${canonicalPath}`);
47
+ }
48
+ if (previous && metadataMatches(previous, metadata) && !options.verifyContentHash) {
49
+ return {
50
+ ...previous,
51
+ file_path: canonicalPath,
52
+ };
53
+ }
54
+ return {
55
+ file_path: canonicalPath,
56
+ ...metadata,
57
+ content_hash: hashBytes(fs.readFileSync(canonicalPath)),
58
+ };
59
+ }
60
+ export function collectFileFingerprints(filePaths, previous = new Map(), options = {}) {
61
+ const files = new Map();
62
+ const missing = new Set();
63
+ for (const filePath of new Set(filePaths)) {
64
+ const candidatePath = resolveCandidatePath(filePath);
65
+ try {
66
+ const previousFingerprint = previous.get(candidatePath);
67
+ const fingerprint = captureFileFingerprint(candidatePath, previousFingerprint, options);
68
+ files.set(fingerprint.file_path, fingerprint);
69
+ }
70
+ catch (error) {
71
+ if (!isMissingFileError(error))
72
+ throw error;
73
+ missing.add(candidatePath);
74
+ }
75
+ }
76
+ return {
77
+ files,
78
+ missing: [...missing].sort(),
79
+ };
80
+ }
81
+ export function compareFileFingerprints(previous, current) {
82
+ const added = [];
83
+ const changed = [];
84
+ const deleted = [];
85
+ const unchanged = [];
86
+ const paths = [...new Set([...previous.keys(), ...current.keys()])].sort();
87
+ for (const filePath of paths) {
88
+ const before = previous.get(filePath);
89
+ const after = current.get(filePath);
90
+ if (!before && after) {
91
+ added.push(filePath);
92
+ }
93
+ else if (before && !after) {
94
+ deleted.push(filePath);
95
+ }
96
+ else if (before && after && before.content_hash !== after.content_hash) {
97
+ changed.push(filePath);
98
+ }
99
+ else {
100
+ unchanged.push(filePath);
101
+ }
102
+ }
103
+ return { added, changed, deleted, unchanged };
104
+ }
@@ -2,6 +2,7 @@ import { TextDecoder } from "node:util";
2
2
  import { lstat, readFile, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { getSourceFileOrThrow } from "./project.js";
5
+ import { NO_REQUEST_CONTEXT } from "./request-context.js";
5
6
  import { hashBytes } from "./workspace.js";
6
7
  export const DEFAULT_FILE_LINE_LIMIT = 200;
7
8
  export const MAX_FILE_LINE_LIMIT = 500;
@@ -58,27 +59,33 @@ function compilerSnapshotHashes(sourceText) {
58
59
  }
59
60
  return hashes;
60
61
  }
61
- export async function readFileSnapshot(project, projectRoot, filePath, options) {
62
+ export async function readFileSnapshot(project, projectRoot, filePath, options, requestContext = NO_REQUEST_CONTEXT) {
63
+ requestContext.checkpoint();
62
64
  assertBoundedRange(options);
63
65
  rejectTraversalPath(filePath);
64
66
  const sourceFile = getSourceFileOrThrow(project, filePath);
67
+ requestContext.checkpoint();
65
68
  const canonicalRoot = await realpath(projectRoot);
66
69
  const sourcePath = sourceFile.getFilePath();
67
70
  const canonicalFile = await realpath(sourcePath);
71
+ requestContext.checkpoint();
68
72
  if (!isContained(canonicalRoot, canonicalFile)) {
69
73
  throw new Error("Source file resolves outside the configured project root.");
70
74
  }
71
75
  const stat = await lstat(canonicalFile);
76
+ requestContext.checkpoint();
72
77
  if (!stat.isFile()) {
73
78
  throw new Error("Source file is not a regular file.");
74
79
  }
75
80
  const bytes = await readFile(canonicalFile);
81
+ requestContext.checkpoint();
76
82
  const source = decodeUtf8(bytes, projectRelative(canonicalRoot, canonicalFile));
77
83
  const fileHash = hashBytes(bytes);
78
84
  const compilerHashes = compilerSnapshotHashes(sourceFile.getFullText());
79
85
  const snapshotState = compilerHashes.includes(fileHash) ? "fresh" : "stale";
80
86
  const allLines = splitLines(source);
81
87
  const selectedLines = allLines.slice(options.offset, options.offset + options.limit);
88
+ requestContext.checkpoint();
82
89
  return {
83
90
  file: projectRelative(canonicalRoot, canonicalFile),
84
91
  range: {