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.
Files changed (35) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +46 -14
  3. package/SECURITY.md +1 -1
  4. package/dist/batch/schema.js +2 -0
  5. package/dist/cli.js +67 -1
  6. package/dist/server.js +2 -0
  7. package/dist/services/agent-setup.js +11 -5
  8. package/dist/services/agent-targets.js +1 -0
  9. package/dist/services/call-spines.js +101 -0
  10. package/dist/services/cli-project-discovery.js +97 -0
  11. package/dist/services/context-builder.js +83 -24
  12. package/dist/services/doctor-continuation.js +37 -0
  13. package/dist/services/doctor-model.js +77 -0
  14. package/dist/services/doctor.js +111 -0
  15. package/dist/services/explore-presentation.js +160 -0
  16. package/dist/services/managed-bundle.js +145 -0
  17. package/dist/services/managed-file.js +37 -0
  18. package/dist/services/project.js +103 -24
  19. package/dist/services/public-errors.js +7 -1
  20. package/dist/services/relationships.js +104 -0
  21. package/dist/services/skill-installer.js +74 -56
  22. package/dist/services/test-candidates.js +15 -8
  23. package/dist/services/upgrade-command.js +77 -0
  24. package/dist/services/upgrade-reconcile.js +41 -0
  25. package/dist/services/upgrade-runtime.js +157 -0
  26. package/dist/services/upgrade.js +232 -0
  27. package/dist/tools/explore.js +73 -2
  28. package/dist/tools/find_test_candidates.js +124 -0
  29. package/dist/tools/get_impact.js +3 -30
  30. package/dist/tools/relationship-schema.js +31 -0
  31. package/docs/support.md +13 -13
  32. package/package.json +2 -2
  33. package/skills/structural-code-editing/SKILL.md +30 -158
  34. package/skills/structural-code-editing/references/operations.md +53 -0
  35. 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);
@@ -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: "index_failed",
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 getOrCreateSession(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
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 watcher = createSessionWatcher(() => session, context.projectRoot, watchDirectories);
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: nextProjectSessionAccessSequence(),
353
+ lastAccessSequence,
336
354
  symbolIndexPolicy,
337
355
  };
338
- projectSessions.set(tsConfigFilePath, session);
339
- session.watcher.start();
340
- if (session.watcher.snapshot().state === "ready") {
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
- export async function getProjectStatus(projectRoot, requestContext = NO_REQUEST_CONTEXT) {
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