ast-mcp-server 0.13.1 → 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.
@@ -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) {
@@ -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 assertExactImpactEvidence(impact) {
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 = neighborsFor(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
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 = neighborsFor(current.endpoint, normalized, requestContext, remainingEdges, false, allowedNeighborKeys, new Set(selectedEdges.keys()));
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 = neighborsFor(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
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 = neighborsFor(current.endpoint, normalized, requestContext, normalized.max_edges - selectedEdges.size, false, new Set(nodes.keys()), new Set(selectedEdges.keys()));
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 = neighborsFor(current.endpoint, normalized, requestContext, 1, true, undefined, new Set(selectedEdges.keys()), IMPACT_PROBE_WORK_ITEMS);
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
- incomplete: truncated,
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 runCoreutils(GNU_LN_PATH, ["-L", "-T", "--", "/proc/self/fd/3", `/proc/self/fd/4/${basename}`], [source.fd, directory.fd]);
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 runCoreutils(GNU_MV_PATH, [
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 = 1;
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(PLAN_SCHEMA_VERSION),
68
- operation: PersistedOperationSchema,
69
- })
70
- .strict();
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 operationId = current.envelope.operation.operation_id;
162
- importOperationRecord(current.envelope.operation, planHash);
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: PLAN_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,