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.
- package/CHANGELOG.md +25 -1
- package/README.md +56 -25
- package/cordis.patch.yml +8 -1
- package/dist/batch/schema.js +1 -1
- package/dist/server.js +15 -1
- package/dist/services/authenticated-publication.js +181 -0
- package/dist/services/call-spines.js +10 -5
- package/dist/services/compiler-worker-host.js +36 -16
- package/dist/services/compiler-worker-protocol.js +70 -2
- package/dist/services/context-builder.js +2 -1
- package/dist/services/diagnostics.js +293 -9
- package/dist/services/h03-timeout-fixture.js +335 -0
- package/dist/services/impact.js +46 -14
- package/dist/services/managed-file.js +3 -50
- package/dist/services/operation-plan-file.js +17 -18
- package/dist/services/operations.js +231 -87
- package/dist/services/project.js +29 -16
- package/dist/services/public-errors.js +7 -5
- package/dist/services/relationships.js +403 -4
- package/dist/services/runtime-policy.js +68 -2
- package/dist/services/test-candidates.js +35 -2
- package/dist/tools/explore.js +5 -3
- package/dist/tools/find_test_candidates.js +24 -3
- package/dist/tools/get_impact.js +12 -3
- package/dist/tools/relationship-schema.js +12 -1
- package/dist/tools/result.js +48 -0
- package/dist/worker-runtime.js +6 -1
- package/docs/support.md +4 -2
- package/package.json +16 -8
- package/skills/structural-code-editing/SKILL.md +8 -0
- package/skills/structural-code-editing/releases.json +1 -1
|
@@ -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) >
|
|
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)) >
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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) {
|