ast-mcp-server 0.11.1 → 0.12.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 (38) hide show
  1. package/CHANGELOG.md +35 -2
  2. package/README.md +32 -2
  3. package/SECURITY.md +1 -1
  4. package/dist/batch/schema.js +7 -16
  5. package/dist/compiler-worker-entry.js +51 -0
  6. package/dist/index.js +20 -120
  7. package/dist/server.js +2 -32
  8. package/dist/services/agent-targets.js +2 -14
  9. package/dist/services/compiler-worker-host.js +422 -0
  10. package/dist/services/compiler-worker-protocol.js +95 -0
  11. package/dist/services/diagnostics.js +55 -0
  12. package/dist/services/operations.js +3 -1
  13. package/dist/services/project.js +7 -0
  14. package/dist/services/public-errors.js +6 -3
  15. package/dist/services/runtime-logger.js +5 -0
  16. package/dist/services/runtime-policy.js +61 -0
  17. package/dist/tools/apply_operation.js +5 -2
  18. package/dist/tools/catalog.js +80 -0
  19. package/dist/tools/explore.js +5 -2
  20. package/dist/tools/find_references.js +5 -2
  21. package/dist/tools/find_test_candidates.js +5 -2
  22. package/dist/tools/get_diagnostics.js +36 -4
  23. package/dist/tools/get_file.js +5 -2
  24. package/dist/tools/get_impact.js +5 -2
  25. package/dist/tools/get_operation_preview.js +5 -2
  26. package/dist/tools/get_outline.js +5 -2
  27. package/dist/tools/get_project_status.js +5 -2
  28. package/dist/tools/get_symbol_source.js +5 -2
  29. package/dist/tools/list_files.js +5 -2
  30. package/dist/tools/rename_symbol.js +5 -2
  31. package/dist/tools/replace_symbol_body.js +5 -2
  32. package/dist/tools/scaffold_class.js +5 -2
  33. package/dist/tools/search_symbols.js +5 -2
  34. package/dist/worker-runtime.js +126 -0
  35. package/docs/support.md +22 -6
  36. package/package.json +3 -3
  37. package/skills/structural-code-editing/SKILL.md +14 -2
  38. package/skills/structural-code-editing/releases.json +17 -2
@@ -0,0 +1,422 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { createInterface } from "node:readline";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { fileURLToPath } from "node:url";
7
+ import { decodeCompilerWorkerEnvelope, startCompilerWorker } from "./compiler-worker-protocol.js";
8
+ import { createCompilerWorkerSpawnSpec, readRuntimePolicy, } from "./runtime-policy.js";
9
+ import { emitCompilerWorkerEvent } from "./runtime-logger.js";
10
+ import { PublicOperationalError, renderPublicError } from "./public-errors.js";
11
+ export class CompilerWorkerHostError extends Error {
12
+ kind;
13
+ recovery;
14
+ constructor(kind, recovery) {
15
+ super("Compiler worker request failed.");
16
+ this.kind = kind;
17
+ this.recovery = recovery;
18
+ }
19
+ }
20
+ const bounded = (value) => Buffer.byteLength(JSON.stringify(value)) <= 256 * 1024;
21
+ const within = (promise, ms) => Promise.race([
22
+ promise,
23
+ delay(ms, undefined, { ref: false }).then(() => {
24
+ throw new CompilerWorkerHostError("startup");
25
+ }),
26
+ ]);
27
+ function deferred() {
28
+ let resolve;
29
+ let reject;
30
+ const promise = new Promise((yes, no) => {
31
+ resolve = yes;
32
+ reject = no;
33
+ });
34
+ return { promise, resolve, reject };
35
+ }
36
+ export function spawnCompilerWorkerProcess(options) {
37
+ const spec = createCompilerWorkerSpawnSpec(options.workerEntryPath, options.environment);
38
+ if (!spec.ok || !spec.command)
39
+ throw new CompilerWorkerHostError("startup");
40
+ const child = spawn(spec.command, spec.args, {
41
+ ...spec.options,
42
+ stdio: ["pipe", "pipe", "inherit", "ipc"],
43
+ });
44
+ const ready = deferred();
45
+ const closed = new Promise((resolve) => child.once("close", () => resolve()));
46
+ const pending = new Map();
47
+ const snapshots = [];
48
+ const acknowledgements = new Map();
49
+ let outbound = 0;
50
+ let inbound = 0;
51
+ let controlInbound = 0;
52
+ let writes = 0;
53
+ const fail = (error) => {
54
+ ready.reject(error);
55
+ for (const request of pending.values())
56
+ request.reject(error);
57
+ pending.clear();
58
+ for (const snapshot of snapshots.splice(0))
59
+ snapshot.reject(error);
60
+ for (const acknowledged of acknowledgements.values())
61
+ acknowledged.reject(error);
62
+ acknowledgements.clear();
63
+ };
64
+ child.once("close", () => fail(new CompilerWorkerHostError("worker_exit")));
65
+ child.on("message", (message) => {
66
+ const frame = JSON.stringify(message);
67
+ const decoded = decodeCompilerWorkerEnvelope(frame);
68
+ if (!decoded.ok ||
69
+ decoded.value.generation !== options.generation ||
70
+ decoded.value.sequence <= controlInbound ||
71
+ !["ready", "snapshot", "settled"].includes(decoded.value.type))
72
+ return fail(new CompilerWorkerHostError("protocol"));
73
+ controlInbound = decoded.value.sequence;
74
+ if (decoded.value.type === "settled") {
75
+ const writeSequence = decoded.value.payload.write_sequence;
76
+ if (!Number.isSafeInteger(writeSequence))
77
+ return fail(new CompilerWorkerHostError("protocol"));
78
+ acknowledgements.get(writeSequence)?.resolve();
79
+ acknowledgements.delete(writeSequence);
80
+ }
81
+ else if (decoded.value.type === "ready") {
82
+ inbound = 1;
83
+ ready.resolve(frame);
84
+ }
85
+ else
86
+ snapshots.shift()?.resolve(decoded.value);
87
+ });
88
+ createInterface({ input: child.stdout }).on("line", (line) => {
89
+ if (Buffer.byteLength(line) > 256 * 1024)
90
+ return fail(new CompilerWorkerHostError("protocol"));
91
+ let message;
92
+ try {
93
+ message = JSON.parse(line);
94
+ }
95
+ catch {
96
+ return fail(new CompilerWorkerHostError("protocol"));
97
+ }
98
+ if (!("id" in message) || message.id === undefined)
99
+ return;
100
+ const request = pending.get(message.id);
101
+ if (!request)
102
+ return;
103
+ pending.delete(message.id);
104
+ request.resolve({
105
+ generation: options.generation,
106
+ sequence: ++inbound,
107
+ payload: { request_id: message.id, message },
108
+ });
109
+ });
110
+ const write = (message, acknowledge = false) => {
111
+ if (!bounded(message) || !child.stdin?.writable)
112
+ return Promise.reject(new CompilerWorkerHostError("protocol"));
113
+ options.beforeWrite?.(message);
114
+ const writeSequence = ++writes, processed = acknowledge ? deferred() : undefined;
115
+ if (processed) {
116
+ acknowledgements.set(writeSequence, processed);
117
+ void processed.promise.catch(() => undefined);
118
+ }
119
+ const written = new Promise((resolve, reject) => child.stdin.write(`${JSON.stringify(message)}\n`, (error) => error ? (acknowledgements.delete(writeSequence), reject(error)) : resolve()));
120
+ return processed ? written.then(() => processed.promise) : written;
121
+ };
122
+ const control = (type, payload) => {
123
+ const frame = { v: 1, generation: options.generation, sequence: ++outbound, type, payload };
124
+ if (!bounded(frame) || !child.connected)
125
+ return Promise.reject(new CompilerWorkerHostError("protocol"));
126
+ return new Promise((resolve, reject) => child.send(frame, (error) => (error ? reject(error) : resolve())));
127
+ };
128
+ return {
129
+ generation: options.generation,
130
+ ready: ready.promise,
131
+ async replay(frames) {
132
+ await control("handshake", {});
133
+ await ready.promise;
134
+ for (const frame of frames)
135
+ await write(frame);
136
+ },
137
+ forward(message) {
138
+ if (!("id" in message))
139
+ return write(message, true).then(() => undefined);
140
+ if (message.id === undefined)
141
+ return Promise.reject(new CompilerWorkerHostError("protocol"));
142
+ const result = deferred();
143
+ const id = message.id;
144
+ pending.set(id, result);
145
+ void write(message).catch((error) => {
146
+ pending.delete(id);
147
+ result.reject(error);
148
+ });
149
+ return result.promise;
150
+ },
151
+ cancel(id) {
152
+ if (!pending.has(id))
153
+ return false;
154
+ void write({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: id } }, true).catch(() => undefined);
155
+ return true;
156
+ },
157
+ async snapshot() {
158
+ await Promise.all([...acknowledgements.values()].map(({ promise }) => promise));
159
+ const result = deferred();
160
+ snapshots.push(result);
161
+ await control("snapshot", {});
162
+ return (await result.promise).payload;
163
+ },
164
+ terminate: () => {
165
+ child.kill("SIGKILL");
166
+ },
167
+ reap: () => closed,
168
+ async shutdown(trigger) {
169
+ await control("shutdown", { trigger }).catch(() => undefined);
170
+ child.stdin?.end();
171
+ await closed;
172
+ return "complete";
173
+ },
174
+ };
175
+ }
176
+ export class CompilerWorkerHost {
177
+ spawn;
178
+ timeoutMs;
179
+ lifecycle;
180
+ generation = 0;
181
+ sequence = 0;
182
+ child;
183
+ starting;
184
+ active = new Map();
185
+ leases = new Map();
186
+ initialization = [];
187
+ activity = 0;
188
+ parentWork = 0;
189
+ parentAdmission = "open";
190
+ idleTimer;
191
+ retiring;
192
+ constructor(spawn, timeoutMs = 10_000, lifecycle = {}) {
193
+ this.spawn = spawn;
194
+ this.timeoutMs = timeoutMs;
195
+ this.lifecycle = lifecycle;
196
+ }
197
+ now() {
198
+ return (this.lifecycle.now ?? Date.now)();
199
+ }
200
+ // prettier-ignore
201
+ pruneLeases() { const now = this.now(); for (const [id, expiry] of this.leases)
202
+ if (expiry <= now)
203
+ this.leases.delete(id); }
204
+ // prettier-ignore
205
+ addLease(id) { if (typeof id !== "string" || !id || !bounded(id))
206
+ return; this.pruneLeases(); this.leases.set(id, this.now() + (this.lifecycle.leaseTtlMs ?? 15 * 60_000)); while (this.leases.size > (this.lifecycle.leaseLimit ?? 128))
207
+ this.leases.delete(this.leases.keys().next().value); }
208
+ // prettier-ignore
209
+ scheduleIdle() { clearTimeout(this.idleTimer); const ttl = this.lifecycle.idleTtlMs ?? 0; if (ttl > 0 && this.child)
210
+ this.idleTimer = setTimeout(() => { this.retiring = this.recycle(this.activity).catch(async () => { const child = this.child; await child?.reap().catch(() => undefined); if (this.child === child)
211
+ this.child = undefined; }).finally(() => { this.retiring = undefined; }); }, ttl); }
212
+ // prettier-ignore
213
+ static quiescent(value) { return value.runtime_admission === "open" && value.project_admission === "open" && ["active_requests", "active_sends", "active_operations", "queued_operations", "completion_critical_operations"].every((key) => value[key] === 0) && value.mutation_history === false; }
214
+ // prettier-ignore
215
+ async recycle(activity) { this.pruneLeases(); const child = this.child, generation = this.generation, sequence = this.sequence; if (!child || this.parentWork || this.leases.size || child.generation !== generation)
216
+ return this.scheduleIdle(); const first = await child.snapshot(); if (!CompilerWorkerHost.quiescent(first) || activity !== this.activity || sequence !== this.sequence)
217
+ return this.scheduleIdle(); this.parentAdmission = "closed"; const second = await child.snapshot(); if (child !== this.child || generation !== this.generation || sequence !== this.sequence || activity !== this.activity || !CompilerWorkerHost.quiescent(second)) {
218
+ this.parentAdmission = "open";
219
+ return this.scheduleIdle();
220
+ } await this.stopChild(child, "requested"); emitCompilerWorkerEvent({ kind: "idle", generation }); this.parentAdmission = "open"; }
221
+ // prettier-ignore
222
+ async stopChild(child, trigger) { const completion = child.shutdown(trigger); try {
223
+ await within(completion, this.timeoutMs);
224
+ if (this.child === child)
225
+ this.child = undefined;
226
+ return "complete";
227
+ }
228
+ catch {
229
+ const snapshot = await child.snapshot().catch(() => undefined);
230
+ if (snapshot?.completion_critical_operations) {
231
+ await completion;
232
+ if (this.child === child)
233
+ this.child = undefined;
234
+ return "complete";
235
+ }
236
+ child.terminate();
237
+ await child.reap().catch(() => undefined);
238
+ if (this.child === child)
239
+ this.child = undefined;
240
+ return "forced";
241
+ } }
242
+ async start(initialization) {
243
+ if (this.child)
244
+ return { generation: this.child.generation, sequence: this.sequence };
245
+ if (this.starting)
246
+ return this.starting;
247
+ this.starting = (async () => {
248
+ let child;
249
+ try {
250
+ const envelope = initialization.length === 2
251
+ ? await startCompilerWorker({
252
+ frames: initialization,
253
+ timeoutMs: this.timeoutMs,
254
+ spawn: () => (child = this.spawn(++this.generation)),
255
+ })
256
+ : await (async () => {
257
+ child = this.spawn(++this.generation);
258
+ await child.replay([]);
259
+ const decoded = decodeCompilerWorkerEnvelope(await within(child.ready, this.timeoutMs));
260
+ if (!decoded.ok || decoded.value.type !== "ready")
261
+ throw new Error();
262
+ return decoded.value;
263
+ })();
264
+ if (!child || envelope.generation !== child.generation)
265
+ throw new Error();
266
+ this.child = child;
267
+ this.sequence = envelope.sequence;
268
+ this.parentAdmission = "open";
269
+ this.scheduleIdle();
270
+ return { generation: child.generation, sequence: this.sequence };
271
+ }
272
+ catch {
273
+ child?.terminate();
274
+ await child?.reap().catch(() => undefined);
275
+ throw new CompilerWorkerHostError("startup");
276
+ }
277
+ })().finally(() => {
278
+ this.starting = undefined;
279
+ });
280
+ return this.starting;
281
+ }
282
+ async forward(message) {
283
+ if (!bounded(message))
284
+ throw new CompilerWorkerHostError("protocol");
285
+ await this.retiring;
286
+ this.activity += 1;
287
+ await this.start(this.initialization.length === 2 ? this.initialization : []);
288
+ this.parentWork += 1;
289
+ const id = "id" in message ? message.id : undefined;
290
+ if (id !== undefined)
291
+ this.active.set(id, this.generation);
292
+ try {
293
+ const settled = await this.child.forward(message);
294
+ if ("method" in message && message.method === "initialize")
295
+ this.initialization = [message];
296
+ if ("method" in message &&
297
+ message.method === "notifications/initialized" &&
298
+ this.initialization.length === 1)
299
+ this.initialization.push(message);
300
+ if (!settled)
301
+ return undefined;
302
+ const payload = settled.payload;
303
+ if (settled.generation !== this.generation ||
304
+ settled.sequence <= this.sequence ||
305
+ payload.request_id !== id ||
306
+ !payload.message)
307
+ throw new CompilerWorkerHostError("protocol");
308
+ this.sequence = settled.sequence;
309
+ const response = payload.message;
310
+ const result = "result" in response ? response.result : undefined;
311
+ this.addLease(result?.operation_id ??
312
+ result?.structuredContent?.operation_id);
313
+ return response;
314
+ }
315
+ catch (error) {
316
+ if (error instanceof CompilerWorkerHostError && error.kind === "worker_exit") {
317
+ const child = this.child;
318
+ await child?.reap().catch(() => undefined);
319
+ if (this.child === child)
320
+ this.child = undefined;
321
+ emitCompilerWorkerEvent({
322
+ kind: "crash",
323
+ generation: child?.generation ?? this.generation,
324
+ });
325
+ const params = "params" in message
326
+ ? message.params
327
+ : undefined;
328
+ const operationId = params?.name === "ast_apply_operation" ? params.arguments?.operation_id : undefined;
329
+ if (typeof operationId === "string") {
330
+ this.addLease(operationId);
331
+ const correlationId = randomUUID();
332
+ emitCompilerWorkerEvent({
333
+ kind: "ambiguity",
334
+ generation: child?.generation ?? this.generation,
335
+ correlationId,
336
+ count: this.leases.size,
337
+ });
338
+ throw new CompilerWorkerHostError("ambiguous_apply", {
339
+ correlation_id: correlationId,
340
+ expires_at: this.leases.get(operationId),
341
+ });
342
+ }
343
+ }
344
+ throw error instanceof CompilerWorkerHostError
345
+ ? error
346
+ : new CompilerWorkerHostError("worker_exit");
347
+ }
348
+ finally {
349
+ if (id !== undefined)
350
+ this.active.delete(id);
351
+ this.parentWork -= 1;
352
+ this.scheduleIdle();
353
+ }
354
+ }
355
+ cancel(id) {
356
+ if (this.active.get(id) !== this.generation || !this.child)
357
+ return "not_found";
358
+ return this.child.cancel(id) ? "forwarded" : "not_found";
359
+ }
360
+ async shutdown(trigger) {
361
+ clearTimeout(this.idleTimer);
362
+ await this.starting?.catch(() => undefined);
363
+ if (!this.child)
364
+ return "complete";
365
+ const child = this.child;
366
+ this.parentAdmission = "closed";
367
+ return this.stopChild(child, trigger);
368
+ }
369
+ snapshot() {
370
+ this.pruneLeases();
371
+ return {
372
+ generation: this.generation,
373
+ sequence: this.sequence,
374
+ state: this.child ? "ready" : this.starting ? "starting" : "idle",
375
+ active_requests: this.active.size,
376
+ parent_work: this.parentWork,
377
+ parent_admission: this.parentAdmission,
378
+ lease_tombstones: this.leases.size,
379
+ };
380
+ }
381
+ }
382
+ export async function runSupervisedStdioServer() {
383
+ const entry = fileURLToPath(new URL("../compiler-worker-entry.js", import.meta.url));
384
+ const policy = readRuntimePolicy(process.env);
385
+ const host = new CompilerWorkerHost((generation) => spawnCompilerWorkerProcess({ generation, workerEntryPath: entry, environment: process.env }), 10_000, { idleTtlMs: policy.compilerWorkerIdleTtlMs });
386
+ const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
387
+ const relay = async (line) => {
388
+ let message;
389
+ try {
390
+ message = JSON.parse(line);
391
+ }
392
+ catch {
393
+ return;
394
+ }
395
+ // prettier-ignore
396
+ if ("method" in message && message.method === "notifications/cancelled") {
397
+ const id = message.params?.requestId;
398
+ if ((typeof id === "string" || typeof id === "number") && bounded(id))
399
+ host.cancel(id);
400
+ return;
401
+ }
402
+ try {
403
+ const response = await host.forward(message);
404
+ if (response)
405
+ process.stdout.write(`${JSON.stringify(response)}\n`);
406
+ }
407
+ catch (error) {
408
+ if ("id" in message) {
409
+ const ambiguous = error instanceof CompilerWorkerHostError &&
410
+ error.kind === "ambiguous_apply" &&
411
+ error.recovery;
412
+ const rendered = ambiguous
413
+ ? renderPublicError(new PublicOperationalError("AMBIGUOUS_APPLY", "The apply outcome is ambiguous."), ambiguous.correlation_id).envelope.error
414
+ : undefined;
415
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32603, message: rendered?.message ?? "Compiler worker request failed.", ...(ambiguous ? { data: { code: "AMBIGUOUS_APPLY", ...ambiguous } } : {}) } })}\n`);
416
+ }
417
+ }
418
+ };
419
+ lines.on("line", (line) => void relay(line));
420
+ lines.once("close", () => void host.shutdown("parent_exit"));
421
+ return { shutdown: () => host.shutdown("requested"), snapshot: () => host.snapshot() };
422
+ }
@@ -0,0 +1,95 @@
1
+ import { Buffer } from "node:buffer";
2
+ const FIELDS = new Set(["v", "generation", "sequence", "type", "payload"]);
3
+ const TYPES = new Set(["handshake", "snapshot", "shutdown", "ready", "settled", "lease", "exit"]);
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ export function decodeCompilerWorkerEnvelope(frame) {
8
+ if (Buffer.byteLength(frame) > 256 * 1024) {
9
+ return { ok: false, reason: "oversized" };
10
+ }
11
+ let value;
12
+ try {
13
+ value = JSON.parse(frame);
14
+ }
15
+ catch {
16
+ return { ok: false, reason: "malformed" };
17
+ }
18
+ if (!isRecord(value))
19
+ return { ok: false, reason: "invalid_envelope" };
20
+ if (Object.keys(value).some((key) => !FIELDS.has(key)))
21
+ return { ok: false, reason: "unknown_field" };
22
+ if (value.v !== 1)
23
+ return { ok: false, reason: "version_drift" };
24
+ if (typeof value.type !== "string" || !TYPES.has(value.type)) {
25
+ return { ok: false, reason: "unknown_type" };
26
+ }
27
+ if (!Number.isSafeInteger(value.generation) ||
28
+ value.generation < 1 ||
29
+ !Number.isSafeInteger(value.sequence) ||
30
+ value.sequence < 1 ||
31
+ !isRecord(value.payload)) {
32
+ return { ok: false, reason: "invalid_envelope" };
33
+ }
34
+ return { ok: true, value };
35
+ }
36
+ export function validateInitializationReplay(frames) {
37
+ if (frames.length < 2)
38
+ return { ok: false, reason: "missing_replay" };
39
+ if (frames.length > 2)
40
+ return { ok: false, reason: "unexpected_replay" };
41
+ const [request, notification] = frames;
42
+ if (!isRecord(request) ||
43
+ request.method !== "initialize" ||
44
+ !("id" in request) ||
45
+ !isRecord(notification) ||
46
+ notification.method !== "notifications/initialized" ||
47
+ "id" in notification) {
48
+ return { ok: false, reason: "missing_replay" };
49
+ }
50
+ if (Buffer.byteLength(JSON.stringify(frames)) > 256 * 1024) {
51
+ return { ok: false, reason: "oversized_replay" };
52
+ }
53
+ return { ok: true, frames: [request, notification] };
54
+ }
55
+ export class CompilerWorkerStartupError extends Error {
56
+ reason;
57
+ constructor(reason) {
58
+ super("Compiler worker startup failed.");
59
+ this.reason = reason;
60
+ }
61
+ }
62
+ async function attempt(options, frames) {
63
+ const child = await options.spawn();
64
+ let timeout;
65
+ try {
66
+ await child.replay(frames);
67
+ const timedOut = new Promise((_, reject) => {
68
+ timeout = setTimeout(() => reject(new CompilerWorkerStartupError("timeout")), options.timeoutMs ?? 10_000);
69
+ });
70
+ const decoded = decodeCompilerWorkerEnvelope(await Promise.race([child.ready, timedOut]));
71
+ if (!decoded.ok || decoded.value.type !== "ready")
72
+ throw new CompilerWorkerStartupError("protocol");
73
+ return decoded.value;
74
+ }
75
+ catch (error) {
76
+ child.terminate();
77
+ await child.reap();
78
+ throw error;
79
+ }
80
+ finally {
81
+ if (timeout !== undefined)
82
+ clearTimeout(timeout);
83
+ }
84
+ }
85
+ export async function startCompilerWorker(options) {
86
+ const replay = validateInitializationReplay(options.frames);
87
+ if (!replay.ok)
88
+ throw new CompilerWorkerStartupError(replay.reason);
89
+ try {
90
+ return await attempt(options, replay.frames);
91
+ }
92
+ catch {
93
+ return attempt(options, replay.frames);
94
+ }
95
+ }
@@ -1,6 +1,61 @@
1
1
  import path from "node:path";
2
2
  import { DiagnosticCategory, ts } from "ts-morph";
3
3
  import { NO_REQUEST_CONTEXT } from "./request-context.js";
4
+ export const DIAGNOSTIC_AGGREGATE_GROUP_LIMIT = 20;
5
+ function safeCount(value) {
6
+ if (!Number.isSafeInteger(value) || value < 0) {
7
+ throw new RangeError("Diagnostic aggregate count is outside the safe integer range.");
8
+ }
9
+ return value;
10
+ }
11
+ function increment(counts, key) {
12
+ counts.set(key, safeCount((counts.get(key) ?? 0) + 1));
13
+ }
14
+ function rankGroups(counts, createGroup, compareKey, groupedDiagnosticCount, requestContext) {
15
+ const groups = [];
16
+ for (const [key, count] of counts) {
17
+ requestContext.checkpoint();
18
+ groups.push(createGroup(key, safeCount(count)));
19
+ }
20
+ groups.sort((left, right) => {
21
+ requestContext.checkpoint();
22
+ return right.count - left.count || compareKey(left, right);
23
+ });
24
+ const selected = groups.slice(0, DIAGNOSTIC_AGGREGATE_GROUP_LIMIT);
25
+ const covered = safeCount(selected.reduce((total, group) => safeCount(total + group.count), 0));
26
+ const omittedGroups = safeCount(groups.length - selected.length);
27
+ return {
28
+ groups: selected,
29
+ total_group_count: safeCount(groups.length),
30
+ omitted_group_count: omittedGroups,
31
+ covered_diagnostic_count: covered,
32
+ omitted_diagnostic_count: safeCount(groupedDiagnosticCount - covered),
33
+ truncated: omittedGroups > 0,
34
+ };
35
+ }
36
+ export function buildDiagnosticAggregates(diagnostics, requestContext = NO_REQUEST_CONTEXT) {
37
+ requestContext.checkpoint();
38
+ const codeCounts = new Map();
39
+ const fileCounts = new Map();
40
+ let unfiled = 0;
41
+ for (const diagnostic of diagnostics) {
42
+ requestContext.checkpoint();
43
+ increment(codeCounts, diagnostic.code);
44
+ if (diagnostic.file === null) {
45
+ unfiled = safeCount(unfiled + 1);
46
+ }
47
+ else {
48
+ increment(fileCounts, diagnostic.file);
49
+ }
50
+ }
51
+ const codes = rankGroups(codeCounts, (code, count) => ({ code, count }), (left, right) => left.code - right.code, safeCount(diagnostics.length), requestContext);
52
+ const files = rankGroups(fileCounts, (file, count) => ({ file, count }), (left, right) => (left.file < right.file ? -1 : left.file > right.file ? 1 : 0), safeCount(diagnostics.length - unfiled), requestContext);
53
+ return {
54
+ group_limit: DIAGNOSTIC_AGGREGATE_GROUP_LIMIT,
55
+ codes,
56
+ files: { ...files, unfiled_diagnostic_count: unfiled },
57
+ };
58
+ }
4
59
  export function normalizeDiagnostic(diagnostic, projectRoot, requestContext = NO_REQUEST_CONTEXT) {
5
60
  requestContext.checkpoint();
6
61
  const sourceFile = diagnostic.getSourceFile();
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { createTwoFilesPatch } from "diff";
6
6
  import { LanguageVariant, Node, ScriptTarget, SyntaxKind, ts } from "ts-morph";
7
7
  import { compareDiagnostics, normalizeDiagnostic, } from "./diagnostics.js";
8
- import { createFreshProject, findDeclarationByName, getSourceFileOrThrow, invalidateProjectIfIdle, withProjectOperation, } from "./project.js";
8
+ import { createFreshProject, findDeclarationByName, getSourceFileOrThrow, invalidateProjectIfIdle, recordProjectMutationHistory, withProjectOperation, } from "./project.js";
9
9
  import { NO_REQUEST_CONTEXT } from "./request-context.js";
10
10
  import { executableDeclaration } from "./symbols.js";
11
11
  import { buildClassScaffold } from "./scaffold.js";
@@ -290,6 +290,7 @@ async function createPlan(projectRootInput, kind, allowNewErrors, mutate, reques
290
290
  };
291
291
  operationContext.checkpoint();
292
292
  operations.set(operationId, record);
293
+ recordProjectMutationHistory();
293
294
  retainedOperationId = operationId;
294
295
  await operationTestHooks.afterRetain?.(operationId);
295
296
  return publicOperation(record);
@@ -846,6 +847,7 @@ export function importOperationRecord(persisted, expectedPlanHash) {
846
847
  }
847
848
  pruneOperations();
848
849
  operations.set(record.operation_id, record);
850
+ recordProjectMutationHistory();
849
851
  return publicOperation(record);
850
852
  }
851
853
  export function setOperationTestHooksForTests(hooks) {
@@ -23,6 +23,7 @@ const projectRoots = new WeakMap();
23
23
  let projectSessionAccessSequence = 0n;
24
24
  let projectRuntimePolicy;
25
25
  let projectRuntimeAdmission = "open";
26
+ let projectMutationHistory = false;
26
27
  function nextProjectSessionAccessSequence() {
27
28
  projectSessionAccessSequence += 1n;
28
29
  return projectSessionAccessSequence;
@@ -791,12 +792,17 @@ export function clearProjectSessions() {
791
792
  projectSessionAccessSequence = 0n;
792
793
  projectRuntimePolicy = undefined;
793
794
  projectRuntimeAdmission = "open";
795
+ projectMutationHistory = false;
796
+ }
797
+ export function recordProjectMutationHistory() {
798
+ projectMutationHistory = true;
794
799
  }
795
800
  export function prepareProjectRuntimeForStartup() {
796
801
  if (projectSessions.size !== 0) {
797
802
  throw new Error("Cannot start a project runtime while sessions remain open.");
798
803
  }
799
804
  projectRuntimeAdmission = "open";
805
+ projectMutationHistory = false;
800
806
  }
801
807
  export function getProjectRuntimeShutdownSnapshot() {
802
808
  let activeOperations = 0;
@@ -814,6 +820,7 @@ export function getProjectRuntimeShutdownSnapshot() {
814
820
  active_operations: activeOperations,
815
821
  queued_operations: queuedOperations,
816
822
  completion_critical_operations: completionCriticalOperations,
823
+ mutation_history: projectMutationHistory,
817
824
  });
818
825
  }
819
826
  export function beginProjectShutdown() {
@@ -15,6 +15,7 @@ export const PUBLIC_ERROR_CODES = Object.freeze([
15
15
  "STALE_WORKSPACE",
16
16
  "INCOMPLETE_EVIDENCE",
17
17
  "MUTATION_BLOCKED",
18
+ "AMBIGUOUS_APPLY",
18
19
  "CONFLICT",
19
20
  "INTERNAL_ERROR",
20
21
  ]);
@@ -38,6 +39,7 @@ const FIXED_PUBLIC_MESSAGES = Object.freeze({
38
39
  STALE_WORKSPACE: "The workspace changed. Retry the operation.",
39
40
  INCOMPLETE_EVIDENCE: "The required compiler evidence is incomplete.",
40
41
  MUTATION_BLOCKED: "The mutation is blocked.",
42
+ AMBIGUOUS_APPLY: "The apply outcome is ambiguous. Inspect the operation receipt before retrying.",
41
43
  CONFLICT: "The operation conflicts with current state.",
42
44
  INTERNAL_ERROR: INTERNAL_ERROR_MESSAGE,
43
45
  });
@@ -178,7 +180,9 @@ function codedError(error) {
178
180
  return null;
179
181
  return {
180
182
  code: code,
181
- message: sanitizePublicText(message),
183
+ message: code === "AMBIGUOUS_APPLY"
184
+ ? FIXED_PUBLIC_MESSAGES.AMBIGUOUS_APPLY
185
+ : sanitizePublicText(message),
182
186
  };
183
187
  }
184
188
  if (!(error instanceof Error))
@@ -236,9 +240,8 @@ function serializePublicError(code, message, correlationId) {
236
240
  };
237
241
  return { envelope, text: JSON.stringify(envelope) };
238
242
  }
239
- export function renderPublicError(error) {
243
+ export function renderPublicError(error, correlationId = randomUUID()) {
240
244
  const classified = classifyPublicError(error);
241
- const correlationId = randomUUID();
242
245
  const initial = serializePublicError(classified.code, classified.message, correlationId);
243
246
  if (Buffer.byteLength(initial.text, "utf8") <= MAX_PUBLIC_ERROR_RESPONSE_BYTES) {
244
247
  return initial;
@@ -48,3 +48,8 @@ export function renderToolFailureEvent(input) {
48
48
  export function emitToolFailureEvent(input) {
49
49
  process.stderr.write(`${renderToolFailureEvent(input)}\n`);
50
50
  }
51
+ // prettier-ignore
52
+ export function emitCompilerWorkerEvent(input) {
53
+ const event = { event: "compiler_worker", version: 1, kind: input.kind, generation: Number.isSafeInteger(input.generation) ? input.generation : 0, correlation_id: input.correlationId && UUID_PATTERN.test(input.correlationId) ? input.correlationId : undefined, count: input.count === undefined ? undefined : Math.max(0, Math.min(128, Math.trunc(input.count))) };
54
+ process.stderr.write(`${JSON.stringify(event)}\n`);
55
+ }