ast-mcp-server 0.13.1 → 0.14.1
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 +26 -4
- package/README.md +24 -10
- package/cordis.patch.yml +1 -1
- package/dist/batch/schema.js +1 -1
- package/dist/services/authenticated-publication.js +181 -0
- package/dist/services/call-spines.js +10 -5
- package/dist/services/context-builder.js +2 -1
- package/dist/services/diagnostics.js +293 -9
- 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/relationships.js +403 -4
- package/dist/services/test-candidates.js +35 -2
- package/dist/tools/find_test_candidates.js +24 -3
- package/dist/tools/get_impact.js +12 -2
- package/dist/tools/relationship-schema.js +12 -1
- package/docs/support.md +4 -2
- package/package.json +9 -7
- package/skills/structural-code-editing/SKILL.md +8 -0
- package/skills/structural-code-editing/releases.json +1 -1
|
@@ -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) {
|
package/dist/services/impact.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createTruncationMetadata, createSourceLocation, } from "./read-contract
|
|
|
3
3
|
import { getSourceFileOrThrow } from "./project.js";
|
|
4
4
|
import { findLocatedDeclaration } from "./symbols.js";
|
|
5
5
|
import { NO_REQUEST_CONTEXT } from "./request-context.js";
|
|
6
|
-
import { createCompilerRelationshipResolver, RELATIONSHIP_EDGE_KINDS, } from "./relationships.js";
|
|
6
|
+
import { createCompilerRelationshipResolver, isRelationshipCoverageComplete, mergeRelationshipCoverage, RELATIONSHIP_EDGE_KINDS, } from "./relationships.js";
|
|
7
7
|
export const IMPACT_DIRECTIONS = Object.freeze(["incoming", "outgoing", "both"]);
|
|
8
8
|
export const DEFAULT_IMPACT_MAX_DEPTH = 3;
|
|
9
9
|
export const DEFAULT_IMPACT_MAX_NODES = 100;
|
|
@@ -24,7 +24,7 @@ export function isExactImpactEdge(edge) {
|
|
|
24
24
|
Array.isArray(edge.freshness.causes) &&
|
|
25
25
|
edge.freshness.causes.length === 0);
|
|
26
26
|
}
|
|
27
|
-
export function
|
|
27
|
+
export function assertCompleteExactImpactEvidence(impact) {
|
|
28
28
|
if (typeof impact !== "object" || impact === null || !Array.isArray(impact.edges)) {
|
|
29
29
|
throw new Error("Impact evidence is invalid.");
|
|
30
30
|
}
|
|
@@ -35,6 +35,9 @@ export function assertExactImpactEvidence(impact) {
|
|
|
35
35
|
throw new Error("Test candidates require fresh exact compiler impact evidence.");
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
export function assertExactImpactEvidence(impact) {
|
|
39
|
+
assertCompleteExactImpactEvidence(impact);
|
|
40
|
+
}
|
|
38
41
|
const IMPACT_PROBE_WORK_ITEMS = 1_024;
|
|
39
42
|
const IMPACT_RELATIONSHIP_WORK_ITEMS = 100_000;
|
|
40
43
|
function assertIntegerInRange(value, minimum, maximum, name) {
|
|
@@ -152,7 +155,7 @@ function collectNeighbors(current, edges, options, requestContext) {
|
|
|
152
155
|
}
|
|
153
156
|
return [...neighbors.values()].sort(neighborOrder);
|
|
154
157
|
}
|
|
155
|
-
function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestContext = NO_REQUEST_CONTEXT) {
|
|
158
|
+
function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestContext = NO_REQUEST_CONTEXT, semanticAuthority = false) {
|
|
156
159
|
requestContext.checkpoint();
|
|
157
160
|
if (typeof root !== "object" || root === null) {
|
|
158
161
|
throw new Error("Impact traversal input is invalid.");
|
|
@@ -165,6 +168,27 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
165
168
|
const queue = [{ key: endpointKey(root), endpoint: root, depth: 0 }];
|
|
166
169
|
const truncationReasons = new Set();
|
|
167
170
|
let maxDepthReached = 0;
|
|
171
|
+
let coverage = [];
|
|
172
|
+
let relationshipWorkItems = 0;
|
|
173
|
+
let relationshipWorkLimitReached = false;
|
|
174
|
+
const resolveNeighbors = (current, providerOptions, context, maxEdges, stopAfterFirst, allowedNeighborKeys, excludedRelationshipIds, maxWorkItems) => {
|
|
175
|
+
const remainingWorkItems = IMPACT_RELATIONSHIP_WORK_ITEMS - relationshipWorkItems;
|
|
176
|
+
if (remainingWorkItems <= 0) {
|
|
177
|
+
relationshipWorkLimitReached = true;
|
|
178
|
+
return {
|
|
179
|
+
neighbors: [],
|
|
180
|
+
incomplete: true,
|
|
181
|
+
excludedNeighbors: false,
|
|
182
|
+
workLimitReached: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const result = neighborsFor(current, providerOptions, context, maxEdges, stopAfterFirst, allowedNeighborKeys, excludedRelationshipIds, Math.min(maxWorkItems ?? remainingWorkItems, remainingWorkItems));
|
|
186
|
+
coverage = mergeRelationshipCoverage(coverage, result.coverage ?? []);
|
|
187
|
+
relationshipWorkItems += result.work?.work_items ?? 0;
|
|
188
|
+
relationshipWorkLimitReached ||=
|
|
189
|
+
result.workLimitReached || result.work?.work_limit_reached === true;
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
168
192
|
const classifyObservedNeighbor = (current, neighbor, edgeBlocked) => {
|
|
169
193
|
const nextKey = endpointKey(neighbor.endpoint);
|
|
170
194
|
const isKnownNode = nodes.has(nextKey);
|
|
@@ -182,7 +206,7 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
182
206
|
const current = queue.shift();
|
|
183
207
|
const remainingEdges = normalized.max_edges - selectedEdges.size;
|
|
184
208
|
if (remainingEdges <= 0) {
|
|
185
|
-
const probe =
|
|
209
|
+
const probe = resolveNeighbors(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
|
|
186
210
|
for (const neighbor of probe.neighbors) {
|
|
187
211
|
classifyObservedNeighbor(current, neighbor, true);
|
|
188
212
|
}
|
|
@@ -196,11 +220,9 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
196
220
|
? "record_limit"
|
|
197
221
|
: undefined;
|
|
198
222
|
const allowedNeighborKeys = restrictionReason ? new Set(nodes.keys()) : undefined;
|
|
199
|
-
const batch =
|
|
223
|
+
const batch = resolveNeighbors(current.endpoint, normalized, requestContext, remainingEdges, false, allowedNeighborKeys, new Set(selectedEdges.keys()));
|
|
200
224
|
if (batch.excludedNeighbors && restrictionReason)
|
|
201
225
|
truncationReasons.add(restrictionReason);
|
|
202
|
-
if (batch.workLimitReached)
|
|
203
|
-
truncationReasons.add("record_limit");
|
|
204
226
|
let skippedNewNode = false;
|
|
205
227
|
for (const neighbor of batch.neighbors) {
|
|
206
228
|
requestContext.checkpoint();
|
|
@@ -236,7 +258,7 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
236
258
|
if (restrictionReason) {
|
|
237
259
|
if (batch.incomplete && !batch.workLimitReached)
|
|
238
260
|
truncationReasons.add("edge_limit");
|
|
239
|
-
const probe =
|
|
261
|
+
const probe = resolveNeighbors(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
|
|
240
262
|
for (const neighbor of probe.neighbors) {
|
|
241
263
|
classifyObservedNeighbor(current, neighbor, selectedEdges.size >= normalized.max_edges);
|
|
242
264
|
}
|
|
@@ -244,11 +266,9 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
244
266
|
}
|
|
245
267
|
if ((skippedNewNode || nodes.size >= normalized.max_nodes) &&
|
|
246
268
|
selectedEdges.size < normalized.max_edges) {
|
|
247
|
-
const knownBatch =
|
|
269
|
+
const knownBatch = resolveNeighbors(current.endpoint, normalized, requestContext, normalized.max_edges - selectedEdges.size, false, new Set(nodes.keys()), new Set(selectedEdges.keys()));
|
|
248
270
|
if (knownBatch.excludedNeighbors)
|
|
249
271
|
truncationReasons.add("record_limit");
|
|
250
|
-
if (knownBatch.workLimitReached)
|
|
251
|
-
truncationReasons.add("record_limit");
|
|
252
272
|
for (const neighbor of knownBatch.neighbors) {
|
|
253
273
|
requestContext.checkpoint();
|
|
254
274
|
if (selectedEdges.size >= normalized.max_edges) {
|
|
@@ -263,7 +283,7 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
263
283
|
}
|
|
264
284
|
else if (batch.incomplete && !batch.workLimitReached) {
|
|
265
285
|
truncationReasons.add("edge_limit");
|
|
266
|
-
const overflowProbe =
|
|
286
|
+
const overflowProbe = resolveNeighbors(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
|
|
267
287
|
for (const neighbor of overflowProbe.neighbors) {
|
|
268
288
|
classifyObservedNeighbor(current, neighbor, true);
|
|
269
289
|
}
|
|
@@ -271,6 +291,9 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
271
291
|
}
|
|
272
292
|
const orderedReasons = ["depth_limit", "record_limit", "edge_limit"].filter((reason) => truncationReasons.has(reason));
|
|
273
293
|
const truncated = orderedReasons.length > 0;
|
|
294
|
+
const boundedIncomplete = truncated || relationshipWorkLimitReached;
|
|
295
|
+
const semanticIncomplete = boundedIncomplete || !isRelationshipCoverageComplete(coverage);
|
|
296
|
+
const incomplete = semanticAuthority ? semanticIncomplete : boundedIncomplete;
|
|
274
297
|
requestContext.checkpoint();
|
|
275
298
|
return {
|
|
276
299
|
root,
|
|
@@ -287,7 +310,14 @@ function traverseWithNeighborProvider(root, neighborsFor, options = {}, requestC
|
|
|
287
310
|
max_depth: normalized.max_depth,
|
|
288
311
|
max_nodes: normalized.max_nodes,
|
|
289
312
|
max_edges: normalized.max_edges,
|
|
290
|
-
|
|
313
|
+
coverage,
|
|
314
|
+
work: {
|
|
315
|
+
work_items: relationshipWorkItems,
|
|
316
|
+
max_work_items: IMPACT_RELATIONSHIP_WORK_ITEMS,
|
|
317
|
+
work_limit_reached: relationshipWorkLimitReached,
|
|
318
|
+
},
|
|
319
|
+
proven_empty: selectedEdges.size === 0 && !semanticIncomplete,
|
|
320
|
+
incomplete,
|
|
291
321
|
truncation: createTruncationMetadata(truncated, truncated ? orderedReasons[0] : null),
|
|
292
322
|
truncation_reasons: orderedReasons,
|
|
293
323
|
};
|
|
@@ -326,9 +356,11 @@ export function traverseCompilerImpact(project, projectRoot, root, freshness, op
|
|
|
326
356
|
});
|
|
327
357
|
return {
|
|
328
358
|
neighbors: collectNeighbors(current, resolution.edges, normalized, requestContext),
|
|
359
|
+
coverage: resolution.coverage,
|
|
360
|
+
work: resolution.work,
|
|
329
361
|
incomplete: resolution.incomplete,
|
|
330
362
|
excludedNeighbors: resolution.excluded_neighbors,
|
|
331
363
|
workLimitReached: resolution.work_limit_reached,
|
|
332
364
|
};
|
|
333
|
-
}, options, requestContext);
|
|
365
|
+
}, options, requestContext, options.authority === "semantic");
|
|
334
366
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { constants as fsConstants } from "node:fs";
|
|
4
3
|
import { lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { runPublicationPrimitive } from "./authenticated-publication.js";
|
|
6
6
|
export class ManagedFileApplyError extends Error {
|
|
7
7
|
commitState;
|
|
8
8
|
rollbackState;
|
|
@@ -23,8 +23,6 @@ export class ManagedFileRollbackError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
const GNU_LN_PATH = "/usr/bin/ln";
|
|
25
25
|
const GNU_MV_PATH = "/usr/bin/mv";
|
|
26
|
-
const COREUTILS_TIMEOUT_MS = 5_000;
|
|
27
|
-
const COREUTILS_STDERR_LIMIT = 4_096;
|
|
28
26
|
function errno(error) {
|
|
29
27
|
return error.code;
|
|
30
28
|
}
|
|
@@ -34,51 +32,6 @@ function objectIdentity(stats) {
|
|
|
34
32
|
function identitiesEqual(left, right) {
|
|
35
33
|
return (left !== undefined && right !== undefined && left.dev === right.dev && left.ino === right.ino);
|
|
36
34
|
}
|
|
37
|
-
async function runCoreutils(executable, args, inheritedDescriptors) {
|
|
38
|
-
await new Promise((resolve, reject) => {
|
|
39
|
-
const child = spawn(executable, args, {
|
|
40
|
-
env: { LANG: "C", LC_ALL: "C", PATH: "/usr/bin:/bin" },
|
|
41
|
-
shell: false,
|
|
42
|
-
stdio: ["ignore", "ignore", "pipe", ...inheritedDescriptors],
|
|
43
|
-
windowsHide: true,
|
|
44
|
-
});
|
|
45
|
-
let stderrBytes = 0;
|
|
46
|
-
let settled = false;
|
|
47
|
-
let requestedFailure;
|
|
48
|
-
const requestFailure = (message, cause) => {
|
|
49
|
-
if (settled || requestedFailure !== undefined)
|
|
50
|
-
return;
|
|
51
|
-
requestedFailure = new Error(message, cause === undefined ? undefined : { cause });
|
|
52
|
-
clearTimeout(timeout);
|
|
53
|
-
child.kill("SIGKILL");
|
|
54
|
-
};
|
|
55
|
-
const timeout = setTimeout(() => requestFailure(`Managed Linux primitive timed out: ${path.basename(executable)}`), COREUTILS_TIMEOUT_MS);
|
|
56
|
-
child.stderr.on("data", (chunk) => {
|
|
57
|
-
stderrBytes += chunk.byteLength;
|
|
58
|
-
if (stderrBytes > COREUTILS_STDERR_LIMIT) {
|
|
59
|
-
requestFailure(`Managed Linux primitive exceeded its stderr limit: ${path.basename(executable)}`);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
child.on("error", (error) => {
|
|
63
|
-
requestFailure(`Managed Linux primitive is unavailable: ${path.basename(executable)}`, error);
|
|
64
|
-
});
|
|
65
|
-
child.on("close", (code, signal) => {
|
|
66
|
-
if (settled)
|
|
67
|
-
return;
|
|
68
|
-
settled = true;
|
|
69
|
-
clearTimeout(timeout);
|
|
70
|
-
if (requestedFailure !== undefined) {
|
|
71
|
-
reject(requestedFailure);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (code === 0 && signal === null) {
|
|
75
|
-
resolve();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
reject(new Error(`Managed Linux primitive failed: ${path.basename(executable)} (${signal ?? code ?? "unknown"})`));
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
35
|
export function hashBytes(content) {
|
|
83
36
|
return createHash("sha256").update(content).digest("hex");
|
|
84
37
|
}
|
|
@@ -425,10 +378,10 @@ async function removePathIfOwned(directory, basename, expectedIdentity) {
|
|
|
425
378
|
return (await capturePathIdentity(directory, basename)) === undefined ? "removed" : "changed";
|
|
426
379
|
}
|
|
427
380
|
async function linkHeldFile(source, directory, basename) {
|
|
428
|
-
await
|
|
381
|
+
await runPublicationPrimitive(GNU_LN_PATH, ["-L", "-T", "--", "/proc/self/fd/3", `/proc/self/fd/4/${basename}`], [source.fd, directory.fd]);
|
|
429
382
|
}
|
|
430
383
|
async function exchangeDirectoryEntries(directory, leftBasename, rightBasename) {
|
|
431
|
-
await
|
|
384
|
+
await runPublicationPrimitive(GNU_MV_PATH, [
|
|
432
385
|
"--exchange",
|
|
433
386
|
"--no-copy",
|
|
434
387
|
"-T",
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { applyOperation, configureOperationApply, exportOperationRecord, importOperationRecord, } from "./operations.js";
|
|
7
7
|
import { ensurePrivateDirectory, resolveRuntimeStateDirectory, } from "./runtime-state.js";
|
|
8
|
-
export const PLAN_SCHEMA_VERSION =
|
|
8
|
+
export const PLAN_SCHEMA_VERSION = 2;
|
|
9
9
|
export const MAX_PLAN_BYTES = 50 * 1024 * 1024;
|
|
10
10
|
const HashSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
11
11
|
const IsoDateSchema = z.string().datetime({ offset: true });
|
|
@@ -62,12 +62,13 @@ const PersistedOperationSchema = z
|
|
|
62
62
|
files: z.array(PersistedFileSchema).min(1),
|
|
63
63
|
})
|
|
64
64
|
.strict();
|
|
65
|
-
const PlanEnvelopeSchema = z
|
|
66
|
-
.object({
|
|
67
|
-
schema_version: z.literal(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const PlanEnvelopeSchema = z.discriminatedUnion("schema_version", [
|
|
66
|
+
z.object({ schema_version: z.literal(1), operation: PersistedOperationSchema }).strict(),
|
|
67
|
+
z.object({ schema_version: z.literal(2), operation: PersistedOperationSchema }).strict(),
|
|
68
|
+
]);
|
|
69
|
+
function planEnvelope(version, operation) {
|
|
70
|
+
return version === 1 ? { schema_version: 1, operation } : { schema_version: 2, operation };
|
|
71
|
+
}
|
|
71
72
|
function stateDirectory(options) {
|
|
72
73
|
return resolveRuntimeStateDirectory(options);
|
|
73
74
|
}
|
|
@@ -147,10 +148,7 @@ async function readPlanEnvelope(planFileInput) {
|
|
|
147
148
|
export async function persistOperationPlan(operationId, options = {}) {
|
|
148
149
|
const plansDirectory = path.join(stateDirectory(options), "plans");
|
|
149
150
|
const planFile = path.join(plansDirectory, `${operationId}.astplan`);
|
|
150
|
-
await atomicWritePlan(planFile,
|
|
151
|
-
schema_version: PLAN_SCHEMA_VERSION,
|
|
152
|
-
operation: exportOperationRecord(operationId),
|
|
153
|
-
});
|
|
151
|
+
await atomicWritePlan(planFile, planEnvelope(PLAN_SCHEMA_VERSION, exportOperationRecord(operationId)));
|
|
154
152
|
return planFile;
|
|
155
153
|
}
|
|
156
154
|
export async function applyPersistedOperation(planFileInput, planHash, options = {}) {
|
|
@@ -158,15 +156,16 @@ export async function applyPersistedOperation(planFileInput, planHash, options =
|
|
|
158
156
|
throw new Error("plan_hash must be a 64-character lowercase SHA-256 digest.");
|
|
159
157
|
}
|
|
160
158
|
const current = await readPlanEnvelope(planFileInput);
|
|
161
|
-
const
|
|
162
|
-
|
|
159
|
+
const { schema_version: planVersion, operation } = current.envelope;
|
|
160
|
+
if (planVersion === 1 && operation.status === "prepared") {
|
|
161
|
+
throw new Error("Legacy v1 prepared plans are not authorized for apply.");
|
|
162
|
+
}
|
|
163
|
+
const operationId = operation.operation_id;
|
|
164
|
+
importOperationRecord(operation, planHash, planVersion);
|
|
163
165
|
configureOperationApply(operationId, {
|
|
164
166
|
stateDirectory: options.stateDirectory,
|
|
165
167
|
receiptWriter: async () => {
|
|
166
|
-
await atomicWritePlan(current.planFile,
|
|
167
|
-
schema_version: PLAN_SCHEMA_VERSION,
|
|
168
|
-
operation: exportOperationRecord(operationId),
|
|
169
|
-
});
|
|
168
|
+
await atomicWritePlan(current.planFile, planEnvelope(planVersion, exportOperationRecord(operationId)));
|
|
170
169
|
},
|
|
171
170
|
});
|
|
172
171
|
return applyOperation(operationId, planHash);
|
|
@@ -175,7 +174,7 @@ export async function inspectPersistedPlan(planFileInput) {
|
|
|
175
174
|
const { envelope } = await readPlanEnvelope(planFileInput);
|
|
176
175
|
const operation = envelope.operation;
|
|
177
176
|
return {
|
|
178
|
-
schema_version:
|
|
177
|
+
schema_version: envelope.schema_version,
|
|
179
178
|
operation_id: operation.operation_id,
|
|
180
179
|
plan_hash: operation.plan_hash,
|
|
181
180
|
status: operation.status,
|