ast-mcp-server 0.13.0 → 0.14.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.
@@ -1,11 +1,79 @@
1
1
  import { Buffer } from "node:buffer";
2
+ export const COMPILER_WORKER_MAX_FRAME_BYTES = 256 * 1024;
3
+ export const COMPILER_WORKER_MAX_REQUEST_ID_BYTES = 8 * 1024;
4
+ export const COMPILER_WORKER_JSONRPC_ENVELOPE_RESERVE_BYTES = 16 * 1024;
5
+ export const COMPILER_WORKER_MAX_RESULT_BYTES = COMPILER_WORKER_MAX_FRAME_BYTES - COMPILER_WORKER_JSONRPC_ENVELOPE_RESERVE_BYTES;
2
6
  const FIELDS = new Set(["v", "generation", "sequence", "type", "payload"]);
3
7
  const TYPES = new Set(["handshake", "snapshot", "shutdown", "ready", "settled", "lease", "exit"]);
4
8
  function isRecord(value) {
5
9
  return typeof value === "object" && value !== null && !Array.isArray(value);
6
10
  }
11
+ export function isCompilerWorkerRequestIdWithinBudget(id) {
12
+ try {
13
+ return Buffer.byteLength(JSON.stringify(id)) <= COMPILER_WORKER_MAX_REQUEST_ID_BYTES;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export function fitsCompilerWorkerResponseResult(result) {
20
+ try {
21
+ const frame = JSON.stringify({
22
+ jsonrpc: "2.0",
23
+ id: "x".repeat(COMPILER_WORKER_MAX_REQUEST_ID_BYTES - 2),
24
+ result,
25
+ });
26
+ return Buffer.byteLength(frame) <= COMPILER_WORKER_MAX_FRAME_BYTES;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export function decodeCompilerWorkerResponse(frame) {
33
+ if (Buffer.byteLength(frame) > COMPILER_WORKER_MAX_FRAME_BYTES) {
34
+ return { ok: false, reason: "oversized" };
35
+ }
36
+ let value;
37
+ try {
38
+ value = JSON.parse(frame);
39
+ }
40
+ catch {
41
+ return { ok: false, reason: "malformed" };
42
+ }
43
+ if (!isRecord(value) || value.jsonrpc !== "2.0") {
44
+ return { ok: false, reason: "invalid_response" };
45
+ }
46
+ if (Object.hasOwn(value, "method") && !Object.hasOwn(value, "id")) {
47
+ if (typeof value.method !== "string" ||
48
+ Object.keys(value).some((key) => !["jsonrpc", "method", "params"].includes(key))) {
49
+ return { ok: false, reason: "invalid_notification" };
50
+ }
51
+ return { ok: true, kind: "notification", value };
52
+ }
53
+ if (Object.keys(value).some((key) => !["jsonrpc", "id", "result", "error"].includes(key))) {
54
+ return { ok: false, reason: "unknown_field" };
55
+ }
56
+ if (!(typeof value.id === "string" || typeof value.id === "number") ||
57
+ !isCompilerWorkerRequestIdWithinBudget(value.id)) {
58
+ return { ok: false, reason: "invalid_id" };
59
+ }
60
+ const hasResult = Object.hasOwn(value, "result");
61
+ const hasError = Object.hasOwn(value, "error");
62
+ if (hasResult === hasError)
63
+ return { ok: false, reason: "invalid_response" };
64
+ if (hasError) {
65
+ const error = value.error;
66
+ if (!isRecord(error) ||
67
+ typeof error.code !== "number" ||
68
+ !Number.isSafeInteger(error.code) ||
69
+ typeof error.message !== "string") {
70
+ return { ok: false, reason: "invalid_error" };
71
+ }
72
+ }
73
+ return { ok: true, kind: "response", value };
74
+ }
7
75
  export function decodeCompilerWorkerEnvelope(frame) {
8
- if (Buffer.byteLength(frame) > 256 * 1024) {
76
+ if (Buffer.byteLength(frame) > COMPILER_WORKER_MAX_FRAME_BYTES) {
9
77
  return { ok: false, reason: "oversized" };
10
78
  }
11
79
  let value;
@@ -47,7 +115,7 @@ export function validateInitializationReplay(frames) {
47
115
  "id" in notification) {
48
116
  return { ok: false, reason: "missing_replay" };
49
117
  }
50
- if (Buffer.byteLength(JSON.stringify(frames)) > 256 * 1024) {
118
+ if (Buffer.byteLength(JSON.stringify(frames)) > COMPILER_WORKER_MAX_FRAME_BYTES) {
51
119
  return { ok: false, reason: "oversized_replay" };
52
120
  }
53
121
  return { ok: true, frames: [request, notification] };
@@ -167,7 +167,8 @@ export async function buildExploreContext(context, request, requestContext = NO_
167
167
  max_depth: request.callSpines.maxDepth,
168
168
  max_nodes: request.callSpines.maxNodes,
169
169
  max_edges: request.callSpines.maxEdges,
170
- discovery_complete: !projected.incomplete,
170
+ discovery_complete: !projected.bounded_incomplete,
171
+ unfinished_gaps: projected.unfinished_gaps,
171
172
  freshness,
172
173
  });
173
174
  }
@@ -56,20 +56,222 @@ export function buildDiagnosticAggregates(diagnostics, requestContext = NO_REQUE
56
56
  files: { ...files, unfiled_diagnostic_count: unfiled },
57
57
  };
58
58
  }
59
- export function normalizeDiagnostic(diagnostic, projectRoot, requestContext = NO_REQUEST_CONTEXT) {
59
+ export function observeDiagnostic(diagnostic, projectRoot, ordinal = 0, requestContext = NO_REQUEST_CONTEXT) {
60
60
  requestContext.checkpoint();
61
61
  const sourceFile = diagnostic.getSourceFile();
62
- const start = diagnostic.getStart();
63
- const location = sourceFile && start !== undefined ? sourceFile.getLineAndColumnAtPos(start) : undefined;
62
+ const start = diagnostic.getStart() ?? null;
63
+ const length = diagnostic.getLength() ?? null;
64
+ const location = sourceFile && start !== null ? sourceFile.getLineAndColumnAtPos(start) : undefined;
64
65
  return {
65
- code: diagnostic.getCode(),
66
- category: DiagnosticCategory[diagnostic.getCategory()] ?? String(diagnostic.getCategory()),
67
- file: sourceFile ? path.relative(projectRoot, sourceFile.getFilePath()) : null,
68
- line: location?.line ?? null,
69
- column: location?.column ?? null,
70
- message: ts.flattenDiagnosticMessageText(diagnostic.compilerObject.messageText, "\n"),
66
+ public: {
67
+ code: diagnostic.getCode(),
68
+ category: DiagnosticCategory[diagnostic.getCategory()] ?? String(diagnostic.getCategory()),
69
+ file: sourceFile ? path.relative(projectRoot, sourceFile.getFilePath()) : null,
70
+ line: location?.line ?? null,
71
+ column: location?.column ?? null,
72
+ message: ts.flattenDiagnosticMessageText(diagnostic.compilerObject.messageText, "\n"),
73
+ },
74
+ start,
75
+ length,
76
+ ordinal,
71
77
  };
72
78
  }
79
+ export function normalizeDiagnostic(diagnostic, projectRoot, requestContext = NO_REQUEST_CONTEXT) {
80
+ return observeDiagnostic(diagnostic, projectRoot, 0, requestContext).public;
81
+ }
82
+ const DEFAULT_EDIT_BUDGET = { frontierSteps: 1_000_000, traceCells: 250_000, hunks: 10_000 };
83
+ const BUDGET_EXCEEDED = Symbol("edit-budget-exceeded");
84
+ const EDIT_CHECKPOINT_INTERVAL = 4_096;
85
+ function createEditWorkTracker(budget) {
86
+ return { budget, work: 0, traceCells: 0, hunks: 0, exhausted: false };
87
+ }
88
+ function exhaust(tracker) {
89
+ tracker.exhausted = true;
90
+ throw BUDGET_EXCEEDED;
91
+ }
92
+ function chargeWork(tracker, requestContext) {
93
+ if (tracker.exhausted || tracker.work >= tracker.budget.frontierSteps)
94
+ exhaust(tracker);
95
+ tracker.work += 1;
96
+ if (tracker.work % EDIT_CHECKPOINT_INTERVAL === 0)
97
+ requestContext.checkpoint();
98
+ }
99
+ function chargeTraceCell(tracker) {
100
+ if (tracker.exhausted || tracker.traceCells >= tracker.budget.traceCells)
101
+ exhaust(tracker);
102
+ tracker.traceCells += 1;
103
+ }
104
+ function chargeHunk(tracker, requestContext) {
105
+ chargeWork(tracker, requestContext);
106
+ if (tracker.hunks >= tracker.budget.hunks)
107
+ exhaust(tracker);
108
+ tracker.hunks += 1;
109
+ }
110
+ function coarseContext(oldLength, newLength, prefix, suffix) {
111
+ const runs = [];
112
+ if (prefix > 0)
113
+ runs.push({ oldStart: 0, oldEnd: prefix, newStart: 0, newEnd: prefix });
114
+ if (suffix > 0) {
115
+ runs.push({
116
+ oldStart: oldLength - suffix,
117
+ oldEnd: oldLength,
118
+ newStart: newLength - suffix,
119
+ newEnd: newLength,
120
+ });
121
+ }
122
+ return {
123
+ coarse: true,
124
+ runs,
125
+ hunks: [
126
+ {
127
+ oldStart: prefix,
128
+ oldEnd: oldLength - suffix,
129
+ newStart: prefix,
130
+ newEnd: newLength - suffix,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ function backtrack(trace, depth, oldLength, newLength, tracker, requestContext) {
136
+ const reversed = [];
137
+ let x = oldLength;
138
+ let y = newLength;
139
+ for (let d = depth; d > 0; d -= 1) {
140
+ requestContext.checkpoint();
141
+ const previous = trace[d - 1];
142
+ const diagonal = x - y;
143
+ const left = previous.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY;
144
+ const right = previous.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY;
145
+ const previousDiagonal = diagonal === -d || (diagonal !== d && left < right) ? diagonal + 1 : diagonal - 1;
146
+ const previousX = previous.get(previousDiagonal);
147
+ const previousY = previousX - previousDiagonal;
148
+ while (x > previousX && y > previousY) {
149
+ chargeWork(tracker, requestContext);
150
+ reversed.push("equal");
151
+ x -= 1;
152
+ y -= 1;
153
+ }
154
+ chargeWork(tracker, requestContext);
155
+ if (x === previousX) {
156
+ reversed.push("insert");
157
+ y -= 1;
158
+ }
159
+ else {
160
+ reversed.push("delete");
161
+ x -= 1;
162
+ }
163
+ }
164
+ while (x > 0 && y > 0) {
165
+ chargeWork(tracker, requestContext);
166
+ reversed.push("equal");
167
+ x -= 1;
168
+ y -= 1;
169
+ }
170
+ return reversed.reverse();
171
+ }
172
+ function emitContext(operations, prefix, tracker, requestContext) {
173
+ const runs = [];
174
+ const hunks = [];
175
+ let oldPosition = prefix;
176
+ let newPosition = prefix;
177
+ if (prefix > 0)
178
+ runs.push({ oldStart: 0, oldEnd: prefix, newStart: 0, newEnd: prefix });
179
+ for (let index = 0; index < operations.length;) {
180
+ requestContext.checkpoint();
181
+ const equal = operations[index] === "equal";
182
+ const oldStart = oldPosition;
183
+ const newStart = newPosition;
184
+ while (index < operations.length && (operations[index] === "equal") === equal) {
185
+ chargeWork(tracker, requestContext);
186
+ if (operations[index] !== "insert")
187
+ oldPosition += 1;
188
+ if (operations[index] !== "delete")
189
+ newPosition += 1;
190
+ index += 1;
191
+ }
192
+ const span = { oldStart, oldEnd: oldPosition, newStart, newEnd: newPosition };
193
+ if (equal)
194
+ runs.push(span);
195
+ else {
196
+ chargeHunk(tracker, requestContext);
197
+ hunks.push(span);
198
+ }
199
+ }
200
+ return { coarse: false, runs, hunks };
201
+ }
202
+ function buildEditContextWithTracker(oldText, newText, tracker, requestContext) {
203
+ requestContext.checkpoint();
204
+ let prefix = 0;
205
+ let suffix = 0;
206
+ try {
207
+ while (prefix < oldText.length && prefix < newText.length) {
208
+ chargeWork(tracker, requestContext);
209
+ if (oldText[prefix] !== newText[prefix])
210
+ break;
211
+ prefix += 1;
212
+ }
213
+ while (suffix < oldText.length - prefix && suffix < newText.length - prefix) {
214
+ chargeWork(tracker, requestContext);
215
+ if (oldText[oldText.length - suffix - 1] !== newText[newText.length - suffix - 1])
216
+ break;
217
+ suffix += 1;
218
+ }
219
+ if (prefix === oldText.length && prefix === newText.length) {
220
+ return {
221
+ coarse: false,
222
+ runs: prefix === 0 ? [] : [{ oldStart: 0, oldEnd: prefix, newStart: 0, newEnd: prefix }],
223
+ hunks: [],
224
+ };
225
+ }
226
+ const oldMiddle = oldText.slice(prefix, oldText.length - suffix);
227
+ const newMiddle = newText.slice(prefix, newText.length - suffix);
228
+ const trace = [];
229
+ let previous = new Map([[1, 0]]);
230
+ for (let depth = 0; depth <= oldMiddle.length + newMiddle.length; depth += 1) {
231
+ const current = new Map();
232
+ for (let diagonal = -depth; diagonal <= depth; diagonal += 2) {
233
+ requestContext.checkpoint();
234
+ chargeWork(tracker, requestContext);
235
+ chargeTraceCell(tracker);
236
+ const left = previous.get(diagonal - 1) ?? Number.NEGATIVE_INFINITY;
237
+ const right = previous.get(diagonal + 1) ?? Number.NEGATIVE_INFINITY;
238
+ let x = diagonal === -depth || (diagonal !== depth && left < right) ? right : left + 1;
239
+ let y = x - diagonal;
240
+ while (x < oldMiddle.length && y < newMiddle.length) {
241
+ chargeWork(tracker, requestContext);
242
+ if (oldMiddle[x] !== newMiddle[y])
243
+ break;
244
+ x += 1;
245
+ y += 1;
246
+ }
247
+ current.set(diagonal, x);
248
+ if (x >= oldMiddle.length && y >= newMiddle.length) {
249
+ trace.push(current);
250
+ const context = emitContext(backtrack(trace, depth, oldMiddle.length, newMiddle.length, tracker, requestContext), prefix, tracker, requestContext);
251
+ if (suffix > 0)
252
+ context.runs.push({
253
+ oldStart: oldText.length - suffix,
254
+ oldEnd: oldText.length,
255
+ newStart: newText.length - suffix,
256
+ newEnd: newText.length,
257
+ });
258
+ return context;
259
+ }
260
+ }
261
+ trace.push(current);
262
+ previous = current;
263
+ }
264
+ throw new Error("Myers alignment did not terminate.");
265
+ }
266
+ catch (error) {
267
+ if (error !== BUDGET_EXCEEDED)
268
+ throw error;
269
+ return coarseContext(oldText.length, newText.length, prefix, suffix);
270
+ }
271
+ }
272
+ export function buildEditContext(oldText, newText, budget = DEFAULT_EDIT_BUDGET, requestContext = NO_REQUEST_CONTEXT) {
273
+ return buildEditContextWithTracker(oldText, newText, createEditWorkTracker(budget), requestContext);
274
+ }
73
275
  function identity(diagnostic) {
74
276
  return JSON.stringify([
75
277
  diagnostic.code,
@@ -78,6 +280,88 @@ function identity(diagnostic) {
78
280
  diagnostic.message,
79
281
  ]);
80
282
  }
283
+ function touchesHunk(start, length, hunks, side) {
284
+ const end = start + length;
285
+ return hunks.some((hunk) => {
286
+ const hunkStart = side === "old" ? hunk.oldStart : hunk.newStart;
287
+ const hunkEnd = side === "old" ? hunk.oldEnd : hunk.newEnd;
288
+ return end >= hunkStart && start <= hunkEnd;
289
+ });
290
+ }
291
+ function observationKey(observation, change, side) {
292
+ const diagnostic = observation.public;
293
+ if (diagnostic.file === null) {
294
+ return JSON.stringify([identity(diagnostic), diagnostic.line, diagnostic.column]);
295
+ }
296
+ if (!change) {
297
+ return JSON.stringify([identity(diagnostic), observation.start, observation.length]);
298
+ }
299
+ if (change.beforeText === null ||
300
+ change.afterText === null ||
301
+ observation.start === null ||
302
+ observation.length === null ||
303
+ !change.context ||
304
+ touchesHunk(observation.start, observation.length, change.context.hunks, side)) {
305
+ return null;
306
+ }
307
+ const run = change.context.runs.find((candidate) => {
308
+ const runStart = side === "old" ? candidate.oldStart : candidate.newStart;
309
+ const runEnd = side === "old" ? candidate.oldEnd : candidate.newEnd;
310
+ return observation.start >= runStart && observation.start + observation.length <= runEnd;
311
+ });
312
+ if (!run)
313
+ return null;
314
+ const mappedStart = side === "old" ? run.newStart + observation.start - run.oldStart : observation.start;
315
+ return JSON.stringify([identity(diagnostic), mappedStart, observation.length]);
316
+ }
317
+ export function compareObservedDiagnostics(before, after, changes, requestContext = NO_REQUEST_CONTEXT) {
318
+ requestContext.checkpoint();
319
+ const edits = new Map();
320
+ const tracker = createEditWorkTracker(DEFAULT_EDIT_BUDGET);
321
+ for (const change of changes) {
322
+ requestContext.checkpoint();
323
+ edits.set(change.file, {
324
+ ...change,
325
+ context: change.beforeText === null || change.afterText === null
326
+ ? null
327
+ : buildEditContextWithTracker(change.beforeText, change.afterText, tracker, requestContext),
328
+ });
329
+ }
330
+ const queues = new Map();
331
+ for (let index = 0; index < before.length; index += 1) {
332
+ requestContext.checkpoint();
333
+ const observation = before[index];
334
+ const key = observationKey(observation, observation.public.file === null ? undefined : edits.get(observation.public.file), "old");
335
+ if (key !== null) {
336
+ const queue = queues.get(key) ?? { indices: [], cursor: 0 };
337
+ queue.indices.push(index);
338
+ queues.set(key, queue);
339
+ }
340
+ }
341
+ const matchedBefore = new Set();
342
+ const matchedAfter = new Set();
343
+ for (let index = 0; index < after.length; index += 1) {
344
+ requestContext.checkpoint();
345
+ const observation = after[index];
346
+ const key = observationKey(observation, observation.public.file === null ? undefined : edits.get(observation.public.file), "new");
347
+ const queue = key === null ? undefined : queues.get(key);
348
+ if (queue && queue.cursor < queue.indices.length) {
349
+ matchedBefore.add(queue.indices[queue.cursor++]);
350
+ matchedAfter.add(index);
351
+ }
352
+ }
353
+ const added = after
354
+ .filter((_, index) => !matchedAfter.has(index))
355
+ .map(({ public: value }) => value);
356
+ const removed = before
357
+ .filter((_, index) => !matchedBefore.has(index))
358
+ .map(({ public: value }) => value);
359
+ return {
360
+ added,
361
+ removed,
362
+ addedErrors: added.filter((diagnostic) => diagnostic.category === "Error"),
363
+ };
364
+ }
81
365
  function subtract(candidates, baseline, requestContext) {
82
366
  const counts = new Map();
83
367
  for (const diagnostic of baseline) {