ast-mcp-server 0.9.2 → 0.11.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 +33 -0
- package/README.md +46 -14
- package/SECURITY.md +1 -1
- package/dist/batch/schema.js +2 -0
- package/dist/cli.js +67 -1
- package/dist/server.js +2 -0
- package/dist/services/agent-setup.js +11 -5
- package/dist/services/agent-targets.js +1 -0
- package/dist/services/call-spines.js +101 -0
- package/dist/services/cli-project-discovery.js +97 -0
- package/dist/services/context-builder.js +83 -24
- package/dist/services/doctor-continuation.js +37 -0
- package/dist/services/doctor-model.js +77 -0
- package/dist/services/doctor.js +111 -0
- package/dist/services/explore-presentation.js +160 -0
- package/dist/services/managed-bundle.js +145 -0
- package/dist/services/managed-file.js +37 -0
- package/dist/services/project.js +103 -24
- package/dist/services/public-errors.js +7 -1
- package/dist/services/relationships.js +104 -0
- package/dist/services/skill-installer.js +74 -56
- package/dist/services/test-candidates.js +15 -8
- package/dist/services/upgrade-command.js +77 -0
- package/dist/services/upgrade-reconcile.js +41 -0
- package/dist/services/upgrade-runtime.js +157 -0
- package/dist/services/upgrade.js +232 -0
- package/dist/tools/explore.js +73 -2
- package/dist/tools/find_test_candidates.js +124 -0
- package/dist/tools/get_impact.js +3 -30
- package/dist/tools/relationship-schema.js +31 -0
- package/docs/support.md +13 -13
- package/package.json +2 -2
- package/skills/structural-code-editing/SKILL.md +30 -158
- package/skills/structural-code-editing/references/operations.md +53 -0
- package/skills/structural-code-editing/releases.json +22 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { applyManagedFilePlan, canonicalizeFuturePath, captureManagedFilePreimage, captureManagedFileSnapshot, createManagedFileApplyContext, hashBytes, ManagedFileApplyError, rollbackManagedFilePlan, snapshotsEqual, verifyManagedFilePostimage, } from "./managed-file.js";
|
|
3
|
+
export class ManagedBundleConflictError extends Error {
|
|
4
|
+
destination;
|
|
5
|
+
constructor(destination) {
|
|
6
|
+
super(`Managed bundle destination already exists with different content: ${destination}`);
|
|
7
|
+
this.destination = destination;
|
|
8
|
+
this.name = "ManagedBundleConflictError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function validateRelease(release) {
|
|
12
|
+
const paths = new Set();
|
|
13
|
+
for (const file of release.files) {
|
|
14
|
+
if (file.path.includes("\\") ||
|
|
15
|
+
path.posix.isAbsolute(file.path) ||
|
|
16
|
+
path.win32.isAbsolute(file.path) ||
|
|
17
|
+
path.posix.normalize(file.path) !== file.path ||
|
|
18
|
+
file.path.startsWith("../") ||
|
|
19
|
+
file.path === ".") {
|
|
20
|
+
throw new Error(`Managed bundle contains an unsafe path: ${file.path}`);
|
|
21
|
+
}
|
|
22
|
+
if (paths.has(file.path))
|
|
23
|
+
throw new Error(`Managed bundle contains a duplicate path: ${file.path}`);
|
|
24
|
+
paths.add(file.path);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function planManagedBundle(options) {
|
|
28
|
+
validateRelease(options.current);
|
|
29
|
+
for (const predecessor of options.predecessors)
|
|
30
|
+
validateRelease(predecessor);
|
|
31
|
+
const signatures = [options.current, ...options.predecessors].map((release) => release.files
|
|
32
|
+
.map((file) => `${file.path}:${file.sha256}`)
|
|
33
|
+
.sort()
|
|
34
|
+
.join("\n"));
|
|
35
|
+
if (new Set(signatures).size !== signatures.length) {
|
|
36
|
+
throw new Error("Managed bundle contains a duplicate release digest.");
|
|
37
|
+
}
|
|
38
|
+
const currentPaths = new Set(options.current.files.map((file) => file.path));
|
|
39
|
+
if (options.predecessors.some((release) => release.files.some((file) => !currentPaths.has(file.path)))) {
|
|
40
|
+
throw new Error("Managed bundle predecessor contains an unmanaged current path.");
|
|
41
|
+
}
|
|
42
|
+
for (const file of options.current.files) {
|
|
43
|
+
if (hashBytes(file.content) !== file.sha256) {
|
|
44
|
+
throw new Error(`Managed bundle digest does not match source asset: ${file.path}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const canonicalRoot = path.dirname(await canonicalizeFuturePath(path.join(options.destinationRoot, ".managed-bundle-root")));
|
|
48
|
+
const destinations = await Promise.all(options.current.files.map(async (file) => ({
|
|
49
|
+
file,
|
|
50
|
+
destination: await canonicalizeFuturePath(path.join(options.destinationRoot, file.path)),
|
|
51
|
+
})));
|
|
52
|
+
for (const { destination } of destinations) {
|
|
53
|
+
const relative = path.relative(canonicalRoot, destination);
|
|
54
|
+
if (relative === "" ||
|
|
55
|
+
relative === ".." ||
|
|
56
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
57
|
+
path.isAbsolute(relative)) {
|
|
58
|
+
throw new Error(`Managed bundle destination is outside the bundle root: ${destination}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (new Set(destinations.map((entry) => entry.destination)).size !== destinations.length) {
|
|
62
|
+
throw new Error("Managed bundle contains a duplicate canonical destination.");
|
|
63
|
+
}
|
|
64
|
+
const entries = await Promise.all(destinations.map(async ({ file, destination }) => ({
|
|
65
|
+
file,
|
|
66
|
+
destination,
|
|
67
|
+
snapshot: await captureManagedFileSnapshot(destination),
|
|
68
|
+
})));
|
|
69
|
+
const exactCurrent = entries.every(({ file, snapshot }) => snapshot.exists && snapshot.sha256 === file.sha256);
|
|
70
|
+
const empty = entries.every(({ snapshot }) => !snapshot.exists);
|
|
71
|
+
const exactPredecessor = options.predecessors.some((release) => entries.every(({ file, snapshot }) => {
|
|
72
|
+
const predecessor = release.files.find((candidate) => candidate.path === file.path);
|
|
73
|
+
return predecessor === undefined
|
|
74
|
+
? !snapshot.exists
|
|
75
|
+
: snapshot.exists && snapshot.sha256 === predecessor.sha256;
|
|
76
|
+
}));
|
|
77
|
+
if (!exactCurrent && !empty && !exactPredecessor && options.force !== true) {
|
|
78
|
+
throw new ManagedBundleConflictError(entries.find(({ snapshot }) => snapshot.exists)?.destination ?? options.destinationRoot);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
status: exactCurrent ? "unchanged" : empty ? "installed" : "updated",
|
|
82
|
+
files: entries.map(({ file, destination, snapshot }) => ({
|
|
83
|
+
path: destination,
|
|
84
|
+
snapshot,
|
|
85
|
+
postimage: file.content,
|
|
86
|
+
postimageSha256: file.sha256,
|
|
87
|
+
status: snapshot.exists && snapshot.sha256 === file.sha256
|
|
88
|
+
? "unchanged"
|
|
89
|
+
: snapshot.exists
|
|
90
|
+
? "updated"
|
|
91
|
+
: "installed",
|
|
92
|
+
})),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function applyManagedBundle(plan, onApplied, context = createManagedFileApplyContext(), hooks = {}) {
|
|
96
|
+
const preimages = new Map();
|
|
97
|
+
for (const file of plan.files) {
|
|
98
|
+
const preimage = await captureManagedFilePreimage(file.path);
|
|
99
|
+
if (!snapshotsEqual(preimage.snapshot, file.snapshot)) {
|
|
100
|
+
throw new Error(`Managed file changed after preflight: ${file.path}`);
|
|
101
|
+
}
|
|
102
|
+
preimages.set(file, preimage);
|
|
103
|
+
}
|
|
104
|
+
const authenticated = [];
|
|
105
|
+
const mutated = [];
|
|
106
|
+
let active;
|
|
107
|
+
try {
|
|
108
|
+
for (const file of plan.files) {
|
|
109
|
+
active = file;
|
|
110
|
+
for (const current of authenticated)
|
|
111
|
+
await verifyManagedFilePostimage(current, context);
|
|
112
|
+
await applyManagedFilePlan(file, context, hooks);
|
|
113
|
+
authenticated.push(file);
|
|
114
|
+
if (file.status !== "unchanged")
|
|
115
|
+
mutated.push(file);
|
|
116
|
+
await onApplied?.(file);
|
|
117
|
+
active = undefined;
|
|
118
|
+
}
|
|
119
|
+
for (const current of authenticated)
|
|
120
|
+
await verifyManagedFilePostimage(current, context);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (active !== undefined &&
|
|
124
|
+
active.status !== "unchanged" &&
|
|
125
|
+
error instanceof ManagedFileApplyError &&
|
|
126
|
+
!mutated.includes(active)) {
|
|
127
|
+
mutated.push(active);
|
|
128
|
+
}
|
|
129
|
+
const failures = [];
|
|
130
|
+
for (const file of mutated.reverse()) {
|
|
131
|
+
try {
|
|
132
|
+
await rollbackManagedFilePlan(file, preimages.get(file), context);
|
|
133
|
+
}
|
|
134
|
+
catch (rollbackError) {
|
|
135
|
+
failures.push(rollbackError);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (failures.length) {
|
|
139
|
+
throw new AggregateError([error, ...failures], "Managed bundle rollback was incomplete.", {
|
|
140
|
+
cause: error,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -499,6 +499,43 @@ export async function verifyManagedFilePostimage(plan, context) {
|
|
|
499
499
|
await directory.close();
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
|
+
export async function rollbackManagedFilePlan(plan, preimage, context) {
|
|
503
|
+
await verifyManagedFilePostimage(plan, context);
|
|
504
|
+
if (preimage.snapshot.exists) {
|
|
505
|
+
if (preimage.content === undefined) {
|
|
506
|
+
throw new Error(`Managed file rollback preimage is unavailable: ${plan.path}`);
|
|
507
|
+
}
|
|
508
|
+
const current = await captureManagedFileSnapshot(plan.path);
|
|
509
|
+
await applyManagedFilePlan({
|
|
510
|
+
path: plan.path,
|
|
511
|
+
snapshot: current,
|
|
512
|
+
postimage: preimage.content,
|
|
513
|
+
postimageSha256: preimage.snapshot.sha256,
|
|
514
|
+
status: "updated",
|
|
515
|
+
}, createManagedFileApplyContext());
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const directory = await openBoundDirectory(plan.snapshot.directory, false, context);
|
|
519
|
+
try {
|
|
520
|
+
const key = managedFileKey(plan);
|
|
521
|
+
let expected = context.authenticatedFiles.get(key);
|
|
522
|
+
if (expected === undefined) {
|
|
523
|
+
const current = await captureManagedFileSnapshot(plan.path);
|
|
524
|
+
if (!current.exists || current.sha256 !== plan.postimageSha256) {
|
|
525
|
+
throw new Error(`Managed installed file rollback postimage is unavailable: ${plan.path}`);
|
|
526
|
+
}
|
|
527
|
+
expected = { dev: current.dev, ino: current.ino };
|
|
528
|
+
context.authenticatedFiles.set(key, expected);
|
|
529
|
+
}
|
|
530
|
+
if ((await removePathIfOwned(directory, path.basename(plan.path), expected)) !== "removed") {
|
|
531
|
+
throw new Error(`Managed installed file could not be rolled back safely: ${plan.path}`);
|
|
532
|
+
}
|
|
533
|
+
context.authenticatedFiles.delete(key);
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
await directory.close();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
502
539
|
export async function applyManagedFilePlan(plan, context = createManagedFileApplyContext(), hooks = {}) {
|
|
503
540
|
if (plan.status === "unchanged") {
|
|
504
541
|
await revalidateManagedFilePlan(plan);
|
package/dist/services/project.js
CHANGED
|
@@ -62,8 +62,7 @@ function fallbackToMemory(session, reason) {
|
|
|
62
62
|
session.context = {
|
|
63
63
|
...session.context,
|
|
64
64
|
status: transitionProjectStatus(session.context.status, {
|
|
65
|
-
type: "
|
|
66
|
-
error: boundedReason,
|
|
65
|
+
type: "index_disabled",
|
|
67
66
|
}),
|
|
68
67
|
symbolIndex: new InMemorySymbolIndex(),
|
|
69
68
|
symbolIndexBackend: "memory",
|
|
@@ -124,6 +123,10 @@ async function ensureSessionSymbolIndex(session, requestContext) {
|
|
|
124
123
|
symbolIndexObservability: createInitialSymbolIndexRuntimeObservability(policy),
|
|
125
124
|
};
|
|
126
125
|
}
|
|
126
|
+
if (session.context.symbolIndexFallbackReason === "capability_unavailable" ||
|
|
127
|
+
(session.context.symbolIndexFallbackReason !== null && policy.backend === "memory")) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
127
130
|
if (policy.backend === "memory") {
|
|
128
131
|
if (policy.mode === "enabled" || policy.mode === "canary") {
|
|
129
132
|
fallbackToMemory(session, policy.reason);
|
|
@@ -234,11 +237,36 @@ function createSessionWatcher(getSession, projectRoot, directories) {
|
|
|
234
237
|
directories,
|
|
235
238
|
onChange: (files) => {
|
|
236
239
|
const session = getSession();
|
|
240
|
+
const cachePath = symbolIndexCachePath(session.symbolIndexPolicy, session.context.status.project);
|
|
241
|
+
let sourceFiles = files;
|
|
242
|
+
if (cachePath) {
|
|
243
|
+
const relativeCachePath = path
|
|
244
|
+
.relative(session.context.projectRoot, cachePath)
|
|
245
|
+
.split(path.sep)
|
|
246
|
+
.join("/");
|
|
247
|
+
if (!relativeCachePath.startsWith("../") && !path.isAbsolute(relativeCachePath)) {
|
|
248
|
+
const relativeCacheDirectory = path.posix.dirname(relativeCachePath);
|
|
249
|
+
const quarantinePrefix = `${relativeCachePath}.corrupt-`;
|
|
250
|
+
sourceFiles = files.filter((file) => {
|
|
251
|
+
if (relativeCacheDirectory !== "." && file === relativeCacheDirectory)
|
|
252
|
+
return false;
|
|
253
|
+
if (file === relativeCachePath ||
|
|
254
|
+
file === `${relativeCachePath}-wal` ||
|
|
255
|
+
file === `${relativeCachePath}-shm`) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return !(file.startsWith(quarantinePrefix) &&
|
|
259
|
+
/^\d+-\d+$/u.test(file.slice(quarantinePrefix.length)));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (sourceFiles.length === 0)
|
|
264
|
+
return;
|
|
237
265
|
session.context = {
|
|
238
266
|
...session.context,
|
|
239
267
|
status: transitionProjectStatus(session.context.status, {
|
|
240
268
|
type: "source_changed",
|
|
241
|
-
files,
|
|
269
|
+
files: sourceFiles,
|
|
242
270
|
}),
|
|
243
271
|
};
|
|
244
272
|
},
|
|
@@ -299,32 +327,22 @@ function assertProjectRuntimeAdmissionOpen() {
|
|
|
299
327
|
throw new ProjectOperationSchedulerError("SERVER_SHUTTING_DOWN");
|
|
300
328
|
}
|
|
301
329
|
}
|
|
302
|
-
function
|
|
303
|
-
assertProjectRuntimeAdmissionOpen();
|
|
330
|
+
function createProjectSession(projectRoot, runtimePolicy, symbolIndexPolicy, requestContext, lastAccessSequence) {
|
|
304
331
|
requestContext.checkpoint();
|
|
305
332
|
const tsConfigFilePath = resolveTsConfigPath(projectRoot);
|
|
306
|
-
const existing = projectSessions.get(tsConfigFilePath);
|
|
307
|
-
if (existing)
|
|
308
|
-
return existing;
|
|
309
|
-
const runtimePolicy = getProjectRuntimePolicy();
|
|
310
|
-
ensureProjectSessionCapacity(runtimePolicy.maxProjectSessions);
|
|
311
|
-
const symbolIndexPolicy = readSymbolIndexPersistencePolicy();
|
|
312
|
-
requestContext.checkpoint();
|
|
313
333
|
const baseContext = buildProjectContext(tsConfigFilePath);
|
|
314
334
|
const context = {
|
|
315
335
|
...baseContext,
|
|
316
336
|
symbolIndexObservability: createInitialSymbolIndexRuntimeObservability(symbolIndexPolicy),
|
|
317
337
|
};
|
|
318
338
|
const watchDirectories = selectProjectWatchDirectories(context.projectRoot, context.project.getSourceFiles().map((sourceFile) => sourceFile.getFilePath()));
|
|
319
|
-
const
|
|
339
|
+
const owner = {};
|
|
340
|
+
const watcher = createSessionWatcher(() => owner.session, context.projectRoot, watchDirectories);
|
|
320
341
|
const session = {
|
|
321
342
|
context,
|
|
322
343
|
watcher,
|
|
323
344
|
watchDirectories,
|
|
324
|
-
configDigest: (
|
|
325
|
-
requestContext.checkpoint();
|
|
326
|
-
return createConfigSnapshot(tsConfigFilePath).digest;
|
|
327
|
-
})(),
|
|
345
|
+
configDigest: createConfigSnapshot(tsConfigFilePath).digest,
|
|
328
346
|
fingerprints: new Map(),
|
|
329
347
|
compilerStateUntrusted: false,
|
|
330
348
|
scheduler: new ProjectOperationScheduler({
|
|
@@ -332,12 +350,12 @@ function getOrCreateSession(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
|
332
350
|
queueWaitTimeoutMs: runtimePolicy.queueWaitTimeoutMs,
|
|
333
351
|
operationDeadlineMs: runtimePolicy.operationDeadlineMs,
|
|
334
352
|
}),
|
|
335
|
-
lastAccessSequence
|
|
353
|
+
lastAccessSequence,
|
|
336
354
|
symbolIndexPolicy,
|
|
337
355
|
};
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (
|
|
356
|
+
owner.session = session;
|
|
357
|
+
watcher.start();
|
|
358
|
+
if (watcher.snapshot().state === "ready") {
|
|
341
359
|
session.context = {
|
|
342
360
|
...session.context,
|
|
343
361
|
status: transitionProjectStatus(session.context.status, { type: "watcher_recovered" }),
|
|
@@ -345,6 +363,20 @@ function getOrCreateSession(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
|
345
363
|
}
|
|
346
364
|
return session;
|
|
347
365
|
}
|
|
366
|
+
function getOrCreateSession(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
367
|
+
assertProjectRuntimeAdmissionOpen();
|
|
368
|
+
requestContext.checkpoint();
|
|
369
|
+
const tsConfigFilePath = resolveTsConfigPath(projectRoot);
|
|
370
|
+
const existing = projectSessions.get(tsConfigFilePath);
|
|
371
|
+
if (existing)
|
|
372
|
+
return existing;
|
|
373
|
+
const runtimePolicy = getProjectRuntimePolicy();
|
|
374
|
+
ensureProjectSessionCapacity(runtimePolicy.maxProjectSessions);
|
|
375
|
+
const symbolIndexPolicy = readSymbolIndexPersistencePolicy();
|
|
376
|
+
const session = createProjectSession(projectRoot, runtimePolicy, symbolIndexPolicy, requestContext, nextProjectSessionAccessSequence());
|
|
377
|
+
projectSessions.set(tsConfigFilePath, session);
|
|
378
|
+
return session;
|
|
379
|
+
}
|
|
348
380
|
function getQueueStatus(session) {
|
|
349
381
|
const snapshot = session.scheduler.snapshot();
|
|
350
382
|
return projectOperationQueueToProjection(snapshot, snapshot.queue_capacity);
|
|
@@ -648,9 +680,7 @@ export async function withProjectOperation(projectRoot, operation, requestContex
|
|
|
648
680
|
return operation(operationContext);
|
|
649
681
|
}, { signal: requestContext.signal });
|
|
650
682
|
}
|
|
651
|
-
|
|
652
|
-
requestContext.checkpoint();
|
|
653
|
-
const session = getOrCreateSession(projectRoot, requestContext);
|
|
683
|
+
function readProjectStatus(session, requestContext) {
|
|
654
684
|
return runSessionWithSyncPolicy(session, async (context, operationContext) => {
|
|
655
685
|
let effectiveContext = context;
|
|
656
686
|
let indexedCount = 0;
|
|
@@ -678,6 +708,55 @@ export async function getProjectStatus(projectRoot, requestContext = NO_REQUEST_
|
|
|
678
708
|
};
|
|
679
709
|
}, true, requestContext);
|
|
680
710
|
}
|
|
711
|
+
function peekSessionStatus(session) {
|
|
712
|
+
const context = session.context;
|
|
713
|
+
return {
|
|
714
|
+
...projectStatusToProjection(context.status),
|
|
715
|
+
index: { state: context.symbolIndexObservability.state },
|
|
716
|
+
indexed_count: 0,
|
|
717
|
+
last_successful_index_at: context.symbolIndexObservability.last_successful_persistence_at,
|
|
718
|
+
index_observability: { ...context.symbolIndexObservability },
|
|
719
|
+
operation_queue: getQueueStatus(session),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
export function peekRegisteredProjectStatus(projectRoot) {
|
|
723
|
+
const session = projectSessions.get(resolveTsConfigPath(projectRoot));
|
|
724
|
+
return session ? peekSessionStatus(session) : undefined;
|
|
725
|
+
}
|
|
726
|
+
export async function getProjectStatus(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
727
|
+
requestContext.checkpoint();
|
|
728
|
+
return readProjectStatus(getOrCreateSession(projectRoot, requestContext), requestContext);
|
|
729
|
+
}
|
|
730
|
+
export async function getDiagnosticProjectStatus(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
731
|
+
requestContext.checkpoint();
|
|
732
|
+
const runtimePolicy = getProjectRuntimePolicy();
|
|
733
|
+
const registered = peekRegisteredProjectStatus(projectRoot);
|
|
734
|
+
if (registered)
|
|
735
|
+
return {
|
|
736
|
+
authority: "registered",
|
|
737
|
+
runtime_admission: projectRuntimeAdmission,
|
|
738
|
+
session_capacity: runtimePolicy.maxProjectSessions,
|
|
739
|
+
status: registered,
|
|
740
|
+
};
|
|
741
|
+
if (projectRuntimeAdmission === "closed")
|
|
742
|
+
return {
|
|
743
|
+
authority: "unavailable",
|
|
744
|
+
runtime_admission: "closed",
|
|
745
|
+
session_capacity: runtimePolicy.maxProjectSessions,
|
|
746
|
+
};
|
|
747
|
+
const session = createProjectSession(projectRoot, runtimePolicy, readSymbolIndexPersistencePolicy({ AST_SYMBOL_INDEX_PERSISTENCE: "disabled" }), requestContext, 0n);
|
|
748
|
+
try {
|
|
749
|
+
return {
|
|
750
|
+
authority: "isolated",
|
|
751
|
+
runtime_admission: projectRuntimeAdmission,
|
|
752
|
+
session_capacity: runtimePolicy.maxProjectSessions,
|
|
753
|
+
status: await readProjectStatus(session, requestContext),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
finally {
|
|
757
|
+
closeProjectSession(session);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
681
760
|
// Compatibility API for internal scripts. MCP tools should use withProject().
|
|
682
761
|
export function getProject(projectRoot) {
|
|
683
762
|
return createFreshProject(projectRoot).project;
|
|
@@ -13,6 +13,7 @@ export const PUBLIC_ERROR_CODES = Object.freeze([
|
|
|
13
13
|
"NOT_FOUND",
|
|
14
14
|
"AMBIGUOUS_TARGET",
|
|
15
15
|
"STALE_WORKSPACE",
|
|
16
|
+
"INCOMPLETE_EVIDENCE",
|
|
16
17
|
"MUTATION_BLOCKED",
|
|
17
18
|
"CONFLICT",
|
|
18
19
|
"INTERNAL_ERROR",
|
|
@@ -35,6 +36,7 @@ const FIXED_PUBLIC_MESSAGES = Object.freeze({
|
|
|
35
36
|
NOT_FOUND: "The requested target was not found.",
|
|
36
37
|
AMBIGUOUS_TARGET: "The requested target is ambiguous.",
|
|
37
38
|
STALE_WORKSPACE: "The workspace changed. Retry the operation.",
|
|
39
|
+
INCOMPLETE_EVIDENCE: "The required compiler evidence is incomplete.",
|
|
38
40
|
MUTATION_BLOCKED: "The mutation is blocked.",
|
|
39
41
|
CONFLICT: "The operation conflicts with current state.",
|
|
40
42
|
INTERNAL_ERROR: INTERNAL_ERROR_MESSAGE,
|
|
@@ -70,6 +72,10 @@ const LEGACY_MESSAGE_RULES = Object.freeze([
|
|
|
70
72
|
code: "STALE_WORKSPACE",
|
|
71
73
|
pattern: /^(?:Workspace changed\b|Project (?:configuration|source) changed during synchronization\.)/,
|
|
72
74
|
},
|
|
75
|
+
{
|
|
76
|
+
code: "INCOMPLETE_EVIDENCE",
|
|
77
|
+
pattern: /^Compiler-backed impact evidence is incomplete\.$/,
|
|
78
|
+
},
|
|
73
79
|
{
|
|
74
80
|
code: "CONFLICT",
|
|
75
81
|
pattern: /^(?:Conflict:|Plan hash mismatch\b|Persisted plan integrity mismatch\b|Operation id collision\b|Applied receipt conflict\b|Postimage recovery conflict\b|Operation target already exists\b|Another AST apply holds\b)/,
|
|
@@ -80,7 +86,7 @@ const LEGACY_MESSAGE_RULES = Object.freeze([
|
|
|
80
86
|
},
|
|
81
87
|
{
|
|
82
88
|
code: "INVALID_INPUT",
|
|
83
|
-
pattern: /^(?:File path traversal is not allowed\.|Windows absolute paths are not valid on this host\.|Source file resolves outside the configured project root\.|Unsupported (?:UTF-16|non-UTF-8) source encoding\b|Persisted (?:operation|applied operation|prepared operation|project_root|original hash|updated hash|affected_files|diagnostic error delta|blocked status)\b|Scaffold file_path must\b|.{0,1024} is not a valid TypeScript identifier\.|.{0,1024} must (?:be|not)\b|.{0,1024} requires\b|.{0,1024} exceeds \d+ characters\.)/,
|
|
89
|
+
pattern: /^(?:File path traversal is not allowed\.|Windows absolute paths are not valid on this host\.|Source file resolves outside the configured project root\.|Unsupported (?:UTF-16|non-UTF-8) source encoding\b|Persisted (?:operation|applied operation|prepared operation|project_root|original hash|updated hash|affected_files|diagnostic error delta|blocked status)\b|Scaffold file_path must\b|.{0,1024} is not a valid TypeScript identifier\.|.{0,1024} must (?:be|not|contain)\b|.{0,1024} requires\b|.{0,1024} exceeds \d+ characters\.)/,
|
|
84
90
|
},
|
|
85
91
|
]);
|
|
86
92
|
export class PublicOperationalError extends Error {
|
|
@@ -1159,6 +1159,110 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
|
|
|
1159
1159
|
},
|
|
1160
1160
|
};
|
|
1161
1161
|
}
|
|
1162
|
+
function callLikeExpression(node) {
|
|
1163
|
+
if (Node.isCallExpression(node) || Node.isNewExpression(node))
|
|
1164
|
+
return node.getExpression();
|
|
1165
|
+
if (Node.isTaggedTemplateExpression(node))
|
|
1166
|
+
return node.getTag();
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
function unwrapInvocationExpression(node) {
|
|
1170
|
+
let current = node;
|
|
1171
|
+
while (Node.isParenthesizedExpression(current) ||
|
|
1172
|
+
Node.isNonNullExpression(current) ||
|
|
1173
|
+
Node.isAsExpression(current) ||
|
|
1174
|
+
Node.isTypeAssertion(current) ||
|
|
1175
|
+
Node.isSatisfiesExpression(current)) {
|
|
1176
|
+
current = current.getExpression();
|
|
1177
|
+
}
|
|
1178
|
+
return current;
|
|
1179
|
+
}
|
|
1180
|
+
function locatedCallTarget(projectRoot, declaration) {
|
|
1181
|
+
if (Node.isParameterDeclaration(declaration) || Node.isMethodSignature(declaration))
|
|
1182
|
+
return undefined;
|
|
1183
|
+
const sourceFile = declaration.getSourceFile();
|
|
1184
|
+
if (!isProjectScopedFile(projectRoot, sourceFile.getFilePath()))
|
|
1185
|
+
return undefined;
|
|
1186
|
+
const targetNode = Node.isConstructorDeclaration(declaration)
|
|
1187
|
+
? declaration.getFirstAncestorByKind(SyntaxKind.ClassDeclaration)
|
|
1188
|
+
: declaration;
|
|
1189
|
+
if (!targetNode)
|
|
1190
|
+
return undefined;
|
|
1191
|
+
return (collectSymbols(sourceFile).find((symbol) => symbol.node === targetNode) ??
|
|
1192
|
+
containingSymbol(sourceFile, targetNode));
|
|
1193
|
+
}
|
|
1194
|
+
export function collectCompilerCallRelationships(project, projectRoot, freshness, options = {}, requestContext = NO_REQUEST_CONTEXT) {
|
|
1195
|
+
const maxEdges = options.max_edges ?? 5_000;
|
|
1196
|
+
const maxWorkItems = options.max_work_items ?? 100_000;
|
|
1197
|
+
if (!Number.isSafeInteger(maxEdges) || maxEdges < 1) {
|
|
1198
|
+
throw new Error("Compiler call max_edges must be a positive safe integer.");
|
|
1199
|
+
}
|
|
1200
|
+
if (!Number.isSafeInteger(maxWorkItems) || maxWorkItems < 1) {
|
|
1201
|
+
throw new Error("Compiler call max_work_items must be a positive safe integer.");
|
|
1202
|
+
}
|
|
1203
|
+
const normalizedFreshness = normalizeFreshness(freshness);
|
|
1204
|
+
const checker = project.getTypeChecker();
|
|
1205
|
+
const edges = new Map();
|
|
1206
|
+
let workItems = 0;
|
|
1207
|
+
let workLimitReached = false;
|
|
1208
|
+
for (const sourceFile of project
|
|
1209
|
+
.getSourceFiles()
|
|
1210
|
+
.sort((left, right) => left.getFilePath().localeCompare(right.getFilePath()))) {
|
|
1211
|
+
sourceFile.forEachDescendant((node, traversal) => {
|
|
1212
|
+
requestContext.checkpoint();
|
|
1213
|
+
if (++workItems > maxWorkItems) {
|
|
1214
|
+
workLimitReached = true;
|
|
1215
|
+
traversal.stop();
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const expression = callLikeExpression(node);
|
|
1219
|
+
if (!expression)
|
|
1220
|
+
return;
|
|
1221
|
+
const invoked = unwrapInvocationExpression(expression);
|
|
1222
|
+
const caller = containingSymbol(sourceFile, node);
|
|
1223
|
+
if (!caller)
|
|
1224
|
+
return;
|
|
1225
|
+
const signature = checker.getResolvedSignature(node);
|
|
1226
|
+
const implicitConstructSignatures = Node.isNewExpression(node)
|
|
1227
|
+
? invoked.getType().getConstructSignatures()
|
|
1228
|
+
: [];
|
|
1229
|
+
if (!signature && implicitConstructSignatures.length !== 1)
|
|
1230
|
+
return;
|
|
1231
|
+
const signatureDeclaration = signature?.getDeclaration();
|
|
1232
|
+
const declarations = declarationsForSymbol(invoked.getSymbol());
|
|
1233
|
+
const targets = new Map();
|
|
1234
|
+
for (const declaration of declarations.length > 0
|
|
1235
|
+
? declarations
|
|
1236
|
+
: signatureDeclaration
|
|
1237
|
+
? [signatureDeclaration]
|
|
1238
|
+
: []) {
|
|
1239
|
+
const target = locatedCallTarget(projectRoot, declaration);
|
|
1240
|
+
if (target)
|
|
1241
|
+
targets.set(symbolEndpoint(target, projectRoot).selector, target);
|
|
1242
|
+
}
|
|
1243
|
+
if (targets.size !== 1)
|
|
1244
|
+
return;
|
|
1245
|
+
const target = [...targets.values()][0];
|
|
1246
|
+
const edge = createRelationshipEdge({
|
|
1247
|
+
source: symbolEndpoint(caller, projectRoot),
|
|
1248
|
+
target: symbolEndpoint(target, projectRoot),
|
|
1249
|
+
kind: "call",
|
|
1250
|
+
provenance: "compiler",
|
|
1251
|
+
confidence: "exact",
|
|
1252
|
+
resolution: "resolved",
|
|
1253
|
+
freshness: normalizedFreshness,
|
|
1254
|
+
});
|
|
1255
|
+
edges.set(edge.relationship_id, edge);
|
|
1256
|
+
});
|
|
1257
|
+
if (workLimitReached)
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
const ordered = [...edges.values()].sort((left, right) => left.relationship_id.localeCompare(right.relationship_id));
|
|
1261
|
+
return Object.freeze({
|
|
1262
|
+
edges: Object.freeze(ordered.slice(0, maxEdges)),
|
|
1263
|
+
incomplete: workLimitReached || ordered.length > maxEdges,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1162
1266
|
export function collectCompilerRelationships(project, projectRoot, freshness, requestContext = NO_REQUEST_CONTEXT) {
|
|
1163
1267
|
requestContext.checkpoint();
|
|
1164
1268
|
const sourceFiles = project
|