ast-mcp-server 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,41 @@ export const RELATIONSHIP_EDGE_KINDS = Object.freeze([
12
12
  "call",
13
13
  "contains",
14
14
  ]);
15
+ export const RELATIONSHIP_COVERAGE_STATUSES = Object.freeze([
16
+ "not_applicable",
17
+ "completed",
18
+ "unsupported",
19
+ "unfinished",
20
+ ]);
21
+ const COVERAGE_STATUS_PRECEDENCE = {
22
+ not_applicable: 0,
23
+ completed: 1,
24
+ unsupported: 2,
25
+ unfinished: 3,
26
+ };
27
+ function coverageKey(entry) {
28
+ return `${entry.kind}:${entry.direction}:${entry.endpoint_class}`;
29
+ }
30
+ export function canonicalRelationshipCoverage(coverage) {
31
+ const kindOrder = new Map(RELATIONSHIP_EDGE_KINDS.map((kind, index) => [kind, index]));
32
+ return [...coverage].sort((left, right) => kindOrder.get(left.kind) - kindOrder.get(right.kind) ||
33
+ left.direction.localeCompare(right.direction) ||
34
+ left.endpoint_class.localeCompare(right.endpoint_class));
35
+ }
36
+ export function mergeRelationshipCoverage(...observations) {
37
+ const merged = new Map();
38
+ for (const entry of observations.flat()) {
39
+ const current = merged.get(coverageKey(entry));
40
+ if (!current ||
41
+ COVERAGE_STATUS_PRECEDENCE[entry.status] > COVERAGE_STATUS_PRECEDENCE[current.status]) {
42
+ merged.set(coverageKey(entry), entry);
43
+ }
44
+ }
45
+ return canonicalRelationshipCoverage([...merged.values()]);
46
+ }
47
+ export function isRelationshipCoverageComplete(coverage) {
48
+ return coverage.every(({ status }) => status === "completed" || status === "not_applicable");
49
+ }
15
50
  export const RELATIONSHIP_PROVENANCES = Object.freeze(["compiler", "syntax", "heuristic"]);
16
51
  export const RELATIONSHIP_CONFIDENCES = Object.freeze(["exact", "high", "medium", "low"]);
17
52
  export const RELATIONSHIP_RESOLUTIONS = Object.freeze([
@@ -268,6 +303,41 @@ function addModuleEdges(state, requestContext) {
268
303
  }
269
304
  }
270
305
  }
306
+ function symbolSupportsCallDirection(node, direction) {
307
+ if (!node)
308
+ return true;
309
+ if (direction === "incoming") {
310
+ return (Node.isFunctionDeclaration(node) ||
311
+ Node.isMethodDeclaration(node) ||
312
+ Node.isConstructorDeclaration(node) ||
313
+ Node.isClassDeclaration(node) ||
314
+ Node.isVariableDeclaration(node));
315
+ }
316
+ return (Node.isFunctionDeclaration(node) ||
317
+ Node.isMethodDeclaration(node) ||
318
+ Node.isConstructorDeclaration(node) ||
319
+ Node.isGetAccessorDeclaration(node) ||
320
+ Node.isSetAccessorDeclaration(node));
321
+ }
322
+ export function relationshipCoverageApplicability(kind, direction, endpointClass, node) {
323
+ if (kind === "contains")
324
+ return true;
325
+ if (endpointClass === "module")
326
+ return kind === "import" || kind === "export";
327
+ if (kind === "reference")
328
+ return true;
329
+ if (kind === "import" || kind === "export")
330
+ return direction === "incoming";
331
+ if (kind === "call")
332
+ return symbolSupportsCallDirection(node, direction);
333
+ if (kind === "extends") {
334
+ return !node || Node.isClassDeclaration(node) || Node.isInterfaceDeclaration(node);
335
+ }
336
+ return (kind === "implements" &&
337
+ (!node ||
338
+ Node.isClassDeclaration(node) ||
339
+ (direction === "incoming" && Node.isInterfaceDeclaration(node))));
340
+ }
271
341
  const DEFAULT_COMPILER_RELATIONSHIP_WORK_ITEMS = 100_000;
272
342
  class ScopedRelationshipLimitReached extends Error {
273
343
  constructor() {
@@ -945,6 +1015,221 @@ function addScopedIncomingExports(state, edges, target) {
945
1015
  });
946
1016
  }
947
1017
  }
1018
+ function scopedInvocationForReference(reference) {
1019
+ for (const ancestor of reference.getAncestors()) {
1020
+ const expression = callLikeExpression(ancestor);
1021
+ if (!expression)
1022
+ continue;
1023
+ if (reference.getStart() < expression.getStart() || reference.getEnd() > expression.getEnd()) {
1024
+ return undefined;
1025
+ }
1026
+ return { invocation: ancestor, direct: unwrapInvocationExpression(expression) === reference };
1027
+ }
1028
+ return undefined;
1029
+ }
1030
+ function scopedLocatedCallTarget(state, collector, declaration) {
1031
+ if (Node.isParameterDeclaration(declaration) || Node.isMethodSignature(declaration))
1032
+ return;
1033
+ const sourceFile = declaration.getSourceFile();
1034
+ if (!isProjectScopedFile(state.projectRoot, sourceFile.getFilePath()))
1035
+ return;
1036
+ const targetNode = Node.isConstructorDeclaration(declaration)
1037
+ ? declaration.getFirstAncestorByKind(SyntaxKind.ClassDeclaration)
1038
+ : declaration;
1039
+ if (!targetNode)
1040
+ return;
1041
+ let located;
1042
+ forEachLocatedSymbol(sourceFile, (candidate) => {
1043
+ consumeScopedWork(state, collector);
1044
+ if (candidate.node !== targetNode)
1045
+ return;
1046
+ located = candidate;
1047
+ return false;
1048
+ });
1049
+ return located;
1050
+ }
1051
+ function computedCallAuthority(projectRoot, invocation) {
1052
+ const expression = callLikeExpression(invocation);
1053
+ if (!expression)
1054
+ return { state: "not_computed", workItems: 0 };
1055
+ const invoked = unwrapInvocationExpression(expression);
1056
+ if (!Node.isElementAccessExpression(invoked))
1057
+ return { state: "not_computed", workItems: 0 };
1058
+ const argument = invoked.getArgumentExpression();
1059
+ const keyTypes = argument
1060
+ ? argument.getType().isUnion()
1061
+ ? argument.getType().getUnionTypes()
1062
+ : [argument.getType()]
1063
+ : [];
1064
+ const keys = keyTypes.flatMap((type) => type.isStringLiteral() && typeof type.getLiteralValue() === "string"
1065
+ ? [type.getLiteralValue()]
1066
+ : []);
1067
+ const receiverType = invoked.getExpression().getType();
1068
+ const receiverTypes = receiverType.isUnion() ? receiverType.getUnionTypes() : [receiverType];
1069
+ const workItems = keyTypes.length + receiverTypes.length;
1070
+ if (keys.length === 1 && keyTypes.length === 1 && receiverTypes.length === 1) {
1071
+ return { state: "single_key_eligible", workItems };
1072
+ }
1073
+ const alternatives = new Map();
1074
+ for (const receiver of receiverTypes) {
1075
+ for (const key of keys) {
1076
+ for (const declaration of declarationsForSymbol(receiver.getProperty(key))) {
1077
+ const target = locatedCallTarget(projectRoot, declaration);
1078
+ if (target)
1079
+ alternatives.set(symbolEndpoint(target, projectRoot).selector, target);
1080
+ }
1081
+ }
1082
+ }
1083
+ const signature = invocation
1084
+ .getSourceFile()
1085
+ .getProject()
1086
+ .getTypeChecker()
1087
+ .getResolvedSignature(invocation);
1088
+ const selected = signature?.getDeclaration();
1089
+ const selectedTarget = selected ? locatedCallTarget(projectRoot, selected) : undefined;
1090
+ if (selectedTarget) {
1091
+ alternatives.set(symbolEndpoint(selectedTarget, projectRoot).selector, selectedTarget);
1092
+ }
1093
+ return {
1094
+ state: "unfinished",
1095
+ alternatives: [...alternatives.values()].sort((left, right) => symbolEndpoint(left, projectRoot).selector.localeCompare(symbolEndpoint(right, projectRoot).selector)),
1096
+ workItems: workItems + alternatives.size,
1097
+ };
1098
+ }
1099
+ function scopedComputedCallSites(state, collector) {
1100
+ if (state.computedCallSites) {
1101
+ reserveScopedWork(state, collector, state.computedCallSites.length);
1102
+ return state.computedCallSites;
1103
+ }
1104
+ const sites = [];
1105
+ for (const sourceFile of scopedSourceFiles(state, collector)) {
1106
+ sourceFile.forEachDescendant((node) => {
1107
+ consumeScopedWork(state, collector);
1108
+ const expression = callLikeExpression(node);
1109
+ if (expression && Node.isElementAccessExpression(unwrapInvocationExpression(expression))) {
1110
+ sites.push(node);
1111
+ }
1112
+ });
1113
+ }
1114
+ state.computedCallSites = sites;
1115
+ return sites;
1116
+ }
1117
+ function scopedDirectCallTarget(state, collector, invocation) {
1118
+ const expression = callLikeExpression(invocation);
1119
+ if (!expression)
1120
+ return;
1121
+ const invoked = unwrapInvocationExpression(expression);
1122
+ if (!Node.isIdentifier(invoked))
1123
+ return;
1124
+ const targets = new Map();
1125
+ for (const declaration of scopedDeclarationsForSymbol(state, collector, invoked.getSymbol())) {
1126
+ const target = scopedLocatedCallTarget(state, collector, declaration);
1127
+ if (target)
1128
+ targets.set(endpointKey(symbolEndpoint(target, state.projectRoot)), target);
1129
+ }
1130
+ return targets.size === 1 ? [...targets.values()][0] : undefined;
1131
+ }
1132
+ function scopedReferenceFlowsToUncertainCall(state, collector, reference) {
1133
+ const declaration = reference.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
1134
+ const initializer = declaration?.getInitializer();
1135
+ const name = declaration?.getNameNode();
1136
+ if (!initializer ||
1137
+ !name ||
1138
+ !Node.isIdentifier(name) ||
1139
+ reference.getStart() < initializer.getStart() ||
1140
+ reference.getEnd() > initializer.getEnd()) {
1141
+ return false;
1142
+ }
1143
+ consumeScopedWork(state, collector);
1144
+ const carrierReferences = name.findReferencesAsNodes();
1145
+ reserveScopedWork(state, collector, carrierReferences.length);
1146
+ return carrierReferences.some((carrierReference) => Boolean(scopedInvocationForReference(carrierReference)));
1147
+ }
1148
+ function addScopedIncomingCalls(state, collector, target) {
1149
+ if (!Node.isReferenceFindable(target.node))
1150
+ return;
1151
+ for (const invocation of scopedComputedCallSites(state, collector)) {
1152
+ const authority = computedCallAuthority(state.projectRoot, invocation);
1153
+ reserveScopedWork(state, collector, authority.workItems);
1154
+ if (authority.state === "unfinished" &&
1155
+ authority.alternatives.some((alternative) => alternative.node === target.node)) {
1156
+ collector.semanticUnfinished = true;
1157
+ }
1158
+ }
1159
+ consumeScopedWork(state, collector);
1160
+ const references = target.node.findReferencesAsNodes();
1161
+ reserveScopedWork(state, collector, references.length);
1162
+ references.sort((left, right) => left.getSourceFile().getFilePath().localeCompare(right.getSourceFile().getFilePath()) ||
1163
+ left.getStart() - right.getStart());
1164
+ const candidates = createScopedCandidateSet(collector);
1165
+ for (const reference of references) {
1166
+ const site = scopedInvocationForReference(reference);
1167
+ if (!site) {
1168
+ if (scopedReferenceFlowsToUncertainCall(state, collector, reference)) {
1169
+ collector.unfinished = true;
1170
+ }
1171
+ continue;
1172
+ }
1173
+ const authority = computedCallAuthority(state.projectRoot, site.invocation);
1174
+ reserveScopedWork(state, collector, authority.workItems);
1175
+ if (authority.state === "unfinished") {
1176
+ collector.semanticUnfinished = true;
1177
+ continue;
1178
+ }
1179
+ const source = scopedContainingSymbol(state, collector, reference.getSourceFile(), site.invocation);
1180
+ const resolved = site.direct
1181
+ ? scopedDirectCallTarget(state, collector, site.invocation)
1182
+ : undefined;
1183
+ if (!source || !resolved || resolved.node !== target.node) {
1184
+ collector.unfinished = true;
1185
+ continue;
1186
+ }
1187
+ candidates.push(createScopedEdge(state, symbolEndpoint(source, state.projectRoot), symbolEndpoint(target, state.projectRoot), "call"));
1188
+ }
1189
+ addScopedCandidates(collector, candidates);
1190
+ }
1191
+ function isNestedNamedCallOwner(node) {
1192
+ if (Node.isFunctionDeclaration(node) ||
1193
+ Node.isClassDeclaration(node) ||
1194
+ Node.isMethodDeclaration(node) ||
1195
+ Node.isConstructorDeclaration(node) ||
1196
+ Node.isGetAccessorDeclaration(node) ||
1197
+ Node.isSetAccessorDeclaration(node)) {
1198
+ return true;
1199
+ }
1200
+ if (!Node.isArrowFunction(node) && !Node.isFunctionExpression(node))
1201
+ return false;
1202
+ const parent = node.getParent();
1203
+ return Boolean(parent &&
1204
+ (Node.isVariableDeclaration(parent) ||
1205
+ Node.isPropertyDeclaration(parent) ||
1206
+ Node.isPropertyAssignment(parent)));
1207
+ }
1208
+ function addScopedOutgoingCalls(state, collector, source) {
1209
+ const candidates = createScopedCandidateSet(collector);
1210
+ source.node.forEachDescendant((node, traversal) => {
1211
+ consumeScopedWork(state, collector);
1212
+ if (isNestedNamedCallOwner(node)) {
1213
+ traversal.skip();
1214
+ return;
1215
+ }
1216
+ if (!callLikeExpression(node))
1217
+ return;
1218
+ const authority = computedCallAuthority(state.projectRoot, node);
1219
+ reserveScopedWork(state, collector, authority.workItems);
1220
+ if (authority.state === "unfinished") {
1221
+ collector.semanticUnfinished = true;
1222
+ return;
1223
+ }
1224
+ const target = scopedDirectCallTarget(state, collector, node);
1225
+ if (!target) {
1226
+ collector.unfinished = true;
1227
+ return;
1228
+ }
1229
+ candidates.push(createScopedEdge(state, symbolEndpoint(source, state.projectRoot), symbolEndpoint(target, state.projectRoot), "call"));
1230
+ });
1231
+ addScopedCandidates(collector, candidates);
1232
+ }
948
1233
  function endpointKey(endpoint) {
949
1234
  return `${endpoint.file}\u0000${endpoint.symbol_path}\u0000${endpoint.selector}`;
950
1235
  }
@@ -966,6 +1251,45 @@ function addScopedIncomingModuleEdges(state, edges) {
966
1251
  addScopedModuleEdges(state, edges, sourceFile);
967
1252
  }
968
1253
  }
1254
+ function requestedCoverageDirections(direction) {
1255
+ return direction === "both" ? ["incoming", "outgoing"] : [direction];
1256
+ }
1257
+ function initialRelationshipCoverage(kinds, direction, endpointClass, node) {
1258
+ return canonicalRelationshipCoverage([...kinds].flatMap((kind) => requestedCoverageDirections(direction).map((coverageDirection) => ({
1259
+ kind,
1260
+ direction: coverageDirection,
1261
+ endpoint_class: endpointClass,
1262
+ status: relationshipCoverageApplicability(kind, coverageDirection, endpointClass, node)
1263
+ ? "unsupported"
1264
+ : "not_applicable",
1265
+ }))));
1266
+ }
1267
+ function relationshipWork(budget, maxWorkItems) {
1268
+ return {
1269
+ work_items: budget.consumed,
1270
+ max_work_items: maxWorkItems,
1271
+ work_limit_reached: budget.limitReached,
1272
+ };
1273
+ }
1274
+ function hasScopedRelationshipProducer(entry) {
1275
+ if (entry.kind === "contains")
1276
+ return false;
1277
+ if (entry.kind === "call")
1278
+ return entry.endpoint_class === "symbol";
1279
+ if (entry.endpoint_class === "module") {
1280
+ return entry.kind === "import" || entry.kind === "export";
1281
+ }
1282
+ if (entry.kind === "reference")
1283
+ return true;
1284
+ if (entry.kind === "import" || entry.kind === "export")
1285
+ return entry.direction === "incoming";
1286
+ return entry.kind === "extends" || entry.kind === "implements";
1287
+ }
1288
+ function resolveProducerCoverage(coverage, interrupted) {
1289
+ return coverage.map((entry) => entry.status === "unsupported" && hasScopedRelationshipProducer(entry)
1290
+ ? { ...entry, status: interrupted ? "unfinished" : "completed" }
1291
+ : entry);
1292
+ }
969
1293
  export function createCompilerRelationshipResolver(project, projectRoot, freshness, requestContext = NO_REQUEST_CONTEXT) {
970
1294
  requestContext.checkpoint();
971
1295
  const state = {
@@ -987,6 +1311,8 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
987
1311
  }
988
1312
  const direction = assertEnum(query.direction, ["incoming", "outgoing", "both"], "direction");
989
1313
  const relationshipKinds = new Set(query.relationship_kinds.map((kind) => assertEnum(kind, RELATIONSHIP_EDGE_KINDS, "edge kind")));
1314
+ const endpointClass = endpoint.symbol_path === "<module>" ? "module" : "symbol";
1315
+ const initialCoverage = initialRelationshipCoverage(relationshipKinds, direction, endpointClass);
990
1316
  const workBudget = {
991
1317
  remaining: maxWorkItems,
992
1318
  consumed: 0,
@@ -1007,6 +1333,8 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
1007
1333
  throw error;
1008
1334
  return {
1009
1335
  edges: [],
1336
+ coverage: resolveProducerCoverage(initialCoverage, true),
1337
+ work: relationshipWork(workBudget, maxWorkItems),
1010
1338
  incomplete: true,
1011
1339
  work_items: workBudget.consumed,
1012
1340
  work_limit_reached: true,
@@ -1028,6 +1356,7 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
1028
1356
  excludedRelationshipIds,
1029
1357
  workBudget,
1030
1358
  excludedNeighbors: false,
1359
+ unfinished: false,
1031
1360
  };
1032
1361
  let located;
1033
1362
  try {
@@ -1038,16 +1367,37 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
1038
1367
  throw error;
1039
1368
  return {
1040
1369
  edges: [],
1370
+ coverage: resolveProducerCoverage(initialCoverage, true),
1371
+ work: relationshipWork(workBudget, maxWorkItems),
1041
1372
  incomplete: true,
1042
1373
  work_items: workBudget.consumed,
1043
1374
  work_limit_reached: true,
1044
1375
  excluded_neighbors: false,
1045
1376
  };
1046
1377
  }
1378
+ const coverage = initialRelationshipCoverage(relationshipKinds, direction, endpointClass, located.symbol?.node);
1047
1379
  const outgoing = direction === "outgoing" || direction === "both";
1048
1380
  const incoming = direction === "incoming" || direction === "both";
1049
1381
  const producers = [];
1050
1382
  if (located.symbol) {
1383
+ if (incoming &&
1384
+ relationshipKinds.has("call") &&
1385
+ relationshipCoverageApplicability("call", "incoming", "symbol", located.symbol.node)) {
1386
+ producers.push({
1387
+ kinds: ["call"],
1388
+ callDirection: "incoming",
1389
+ collect: (collector) => addScopedIncomingCalls(state, collector, located.symbol),
1390
+ });
1391
+ }
1392
+ if (outgoing &&
1393
+ relationshipKinds.has("call") &&
1394
+ relationshipCoverageApplicability("call", "outgoing", "symbol", located.symbol.node)) {
1395
+ producers.push({
1396
+ kinds: ["call"],
1397
+ callDirection: "outgoing",
1398
+ collect: (collector) => addScopedOutgoingCalls(state, collector, located.symbol),
1399
+ });
1400
+ }
1051
1401
  if (outgoing && (relationshipKinds.has("extends") || relationshipKinds.has("implements"))) {
1052
1402
  producers.push({
1053
1403
  kinds: ["extends", "implements"],
@@ -1113,7 +1463,9 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
1113
1463
  }
1114
1464
  }
1115
1465
  const merged = new Map();
1466
+ const callCoverage = new Map(producers.flatMap((producer) => producer.callDirection ? [[producer.callDirection, "unfinished"]] : []));
1116
1467
  let incomplete = false;
1468
+ let boundedIncomplete = false;
1117
1469
  let excludedNeighbors = false;
1118
1470
  for (const producer of producers) {
1119
1471
  const collector = {
@@ -1128,7 +1480,9 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
1128
1480
  excludedRelationshipIds,
1129
1481
  workBudget,
1130
1482
  excludedNeighbors: false,
1483
+ unfinished: false,
1131
1484
  };
1485
+ let interrupted = false;
1132
1486
  try {
1133
1487
  producer.collect(collector);
1134
1488
  }
@@ -1137,8 +1491,18 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
1137
1491
  !(error instanceof ScopedRelationshipWorkLimitReached)) {
1138
1492
  throw error;
1139
1493
  }
1494
+ interrupted = true;
1495
+ boundedIncomplete = true;
1140
1496
  incomplete = true;
1141
1497
  }
1498
+ if (collector.unfinished)
1499
+ incomplete = true;
1500
+ if (producer.callDirection &&
1501
+ !interrupted &&
1502
+ !collector.unfinished &&
1503
+ !collector.semanticUnfinished) {
1504
+ callCoverage.set(producer.callDirection, "completed");
1505
+ }
1142
1506
  for (const edge of collector.edges.values())
1143
1507
  merged.set(edge.relationship_id, edge);
1144
1508
  excludedNeighbors ||= collector.excludedNeighbors;
@@ -1146,11 +1510,18 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
1146
1510
  break;
1147
1511
  }
1148
1512
  const ordered = [...merged.values()].sort((left, right) => scopedNeighborOrder(key, left, right));
1149
- if (ordered.length > query.max_edges)
1513
+ if (ordered.length > query.max_edges) {
1150
1514
  incomplete = true;
1515
+ boundedIncomplete = true;
1516
+ }
1151
1517
  const selected = ordered.slice(0, query.max_edges);
1518
+ const resolvedCoverage = resolveProducerCoverage(coverage, boundedIncomplete || workBudget.limitReached).map((entry) => entry.kind === "call" && callCoverage.has(entry.direction)
1519
+ ? { ...entry, status: callCoverage.get(entry.direction) }
1520
+ : entry);
1152
1521
  return {
1153
1522
  edges: selected.sort((left, right) => left.relationship_id.localeCompare(right.relationship_id)),
1523
+ coverage: resolvedCoverage,
1524
+ work: relationshipWork(workBudget, maxWorkItems),
1154
1525
  incomplete,
1155
1526
  work_items: workBudget.consumed,
1156
1527
  work_limit_reached: workBudget.limitReached,
@@ -1203,6 +1574,7 @@ export function collectCompilerCallRelationships(project, projectRoot, freshness
1203
1574
  const normalizedFreshness = normalizeFreshness(freshness);
1204
1575
  const checker = project.getTypeChecker();
1205
1576
  const edges = new Map();
1577
+ const unfinishedGaps = [];
1206
1578
  let workItems = 0;
1207
1579
  let workLimitReached = false;
1208
1580
  for (const sourceFile of project
@@ -1222,6 +1594,20 @@ export function collectCompilerCallRelationships(project, projectRoot, freshness
1222
1594
  const caller = containingSymbol(sourceFile, node);
1223
1595
  if (!caller)
1224
1596
  return;
1597
+ const computedAuthority = computedCallAuthority(projectRoot, node);
1598
+ workItems += computedAuthority.workItems;
1599
+ if (workItems > maxWorkItems) {
1600
+ workLimitReached = true;
1601
+ traversal.stop();
1602
+ return;
1603
+ }
1604
+ if (computedAuthority.state === "unfinished") {
1605
+ unfinishedGaps.push({
1606
+ source: symbolEndpoint(caller, projectRoot),
1607
+ alternatives: computedAuthority.alternatives.map((target) => symbolEndpoint(target, projectRoot)),
1608
+ });
1609
+ return;
1610
+ }
1225
1611
  const signature = checker.getResolvedSignature(node);
1226
1612
  const implicitConstructSignatures = Node.isNewExpression(node)
1227
1613
  ? invoked.getType().getConstructSignatures()
@@ -1231,6 +1617,7 @@ export function collectCompilerCallRelationships(project, projectRoot, freshness
1231
1617
  const signatureDeclaration = signature?.getDeclaration();
1232
1618
  const declarations = declarationsForSymbol(invoked.getSymbol());
1233
1619
  const targets = new Map();
1620
+ let unfinished = false;
1234
1621
  for (const declaration of declarations.length > 0
1235
1622
  ? declarations
1236
1623
  : signatureDeclaration
@@ -1238,10 +1625,18 @@ export function collectCompilerCallRelationships(project, projectRoot, freshness
1238
1625
  : []) {
1239
1626
  const target = locatedCallTarget(projectRoot, declaration);
1240
1627
  if (target)
1241
- targets.set(symbolEndpoint(target, projectRoot).selector, target);
1628
+ targets.set(endpointKey(symbolEndpoint(target, projectRoot)), target);
1629
+ else
1630
+ unfinished = true;
1242
1631
  }
1243
- if (targets.size !== 1)
1632
+ // Dropping an unlocatable alternative does not establish a single exact target.
1633
+ if (unfinished || targets.size !== 1) {
1634
+ unfinishedGaps.push({
1635
+ source: symbolEndpoint(caller, projectRoot),
1636
+ alternatives: [...targets.values()].map((target) => symbolEndpoint(target, projectRoot)),
1637
+ });
1244
1638
  return;
1639
+ }
1245
1640
  const target = [...targets.values()][0];
1246
1641
  const edge = createRelationshipEdge({
1247
1642
  source: symbolEndpoint(caller, projectRoot),
@@ -1258,9 +1653,13 @@ export function collectCompilerCallRelationships(project, projectRoot, freshness
1258
1653
  break;
1259
1654
  }
1260
1655
  const ordered = [...edges.values()].sort((left, right) => left.relationship_id.localeCompare(right.relationship_id));
1656
+ const boundedIncomplete = workLimitReached || ordered.length > maxEdges;
1657
+ unfinishedGaps.sort((left, right) => endpointKey(left.source).localeCompare(endpointKey(right.source)));
1261
1658
  return Object.freeze({
1262
1659
  edges: Object.freeze(ordered.slice(0, maxEdges)),
1263
- incomplete: workLimitReached || ordered.length > maxEdges,
1660
+ unfinished_gaps: Object.freeze(unfinishedGaps),
1661
+ bounded_incomplete: boundedIncomplete,
1662
+ incomplete: boundedIncomplete || unfinishedGaps.length > 0,
1264
1663
  });
1265
1664
  }
1266
1665
  export function collectCompilerRelationships(project, projectRoot, freshness, requestContext = NO_REQUEST_CONTEXT) {
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import packageMetadata from "../../package.json" with { type: "json" };
2
3
  export const RUNTIME_POLICY_ENV_KEYS = Object.freeze([
3
4
  "AST_MAX_PROJECT_SESSIONS",
4
5
  "AST_MAX_QUEUED_OPERATIONS_PER_PROJECT",
@@ -8,6 +9,7 @@ export const RUNTIME_POLICY_ENV_KEYS = Object.freeze([
8
9
  "AST_COMPILER_WORKER_MODE",
9
10
  "AST_COMPILER_WORKER_IDLE_TTL_MS",
10
11
  "AST_MCP_APPLY_GUARD",
12
+ "AST_MCP_TEXT_PROJECTION",
11
13
  ]);
12
14
  function parseBoundedInteger(rawValue, defaultValue, minimum, maximum) {
13
15
  if (rawValue === undefined)
@@ -27,8 +29,8 @@ function parseBoundedInteger(rawValue, defaultValue, minimum, maximum) {
27
29
  export function parseRuntimePolicy(environment) {
28
30
  const maxProjectSessions = parseBoundedInteger(environment.AST_MAX_PROJECT_SESSIONS, 8, 1, 32);
29
31
  const maxQueuedOperationsPerProject = parseBoundedInteger(environment.AST_MAX_QUEUED_OPERATIONS_PER_PROJECT, 32, 1, 256);
30
- const queueWaitTimeoutMs = parseBoundedInteger(environment.AST_QUEUE_WAIT_TIMEOUT_MS, 30_000, 100, 300_000);
31
- const operationDeadlineMs = parseBoundedInteger(environment.AST_OPERATION_DEADLINE_MS, 120_000, 1_000, 900_000);
32
+ const queueWaitTimeoutMs = parseBoundedInteger(environment.AST_QUEUE_WAIT_TIMEOUT_MS, packageMetadata.deepseekHarness.timeoutBudget.queueWaitMs, 100, 300_000);
33
+ const operationDeadlineMs = parseBoundedInteger(environment.AST_OPERATION_DEADLINE_MS, packageMetadata.deepseekHarness.timeoutBudget.executionDeadlineMs, 1_000, 900_000);
32
34
  const shutdownDrainTimeoutMs = parseBoundedInteger(environment.AST_SHUTDOWN_DRAIN_TIMEOUT_MS, 10_000, 100, 60_000);
33
35
  const rawWorkerMode = environment.AST_COMPILER_WORKER_MODE;
34
36
  const compilerWorkerMode = rawWorkerMode === "supervised" ? "supervised" : "in_process";
@@ -47,6 +49,13 @@ export function parseRuntimePolicy(environment) {
47
49
  : rawApplyGuard === "allow" || rawApplyGuard === "deny"
48
50
  ? "configured"
49
51
  : "invalid_mode";
52
+ const rawTextProjection = environment.AST_MCP_TEXT_PROJECTION;
53
+ const projectStructuredContentAsText = rawTextProjection === "canonical_json";
54
+ const textProjectionReason = rawTextProjection === undefined
55
+ ? "default"
56
+ : projectStructuredContentAsText
57
+ ? "configured"
58
+ : "invalid_mode";
50
59
  const reasons = Object.freeze({
51
60
  AST_MAX_PROJECT_SESSIONS: maxProjectSessions.reason,
52
61
  AST_MAX_QUEUED_OPERATIONS_PER_PROJECT: maxQueuedOperationsPerProject.reason,
@@ -56,6 +65,7 @@ export function parseRuntimePolicy(environment) {
56
65
  AST_COMPILER_WORKER_MODE: compilerWorkerModeReason,
57
66
  AST_COMPILER_WORKER_IDLE_TTL_MS: compilerWorkerIdleTtlMs.reason,
58
67
  AST_MCP_APPLY_GUARD: applyGuardReason,
68
+ AST_MCP_TEXT_PROJECTION: textProjectionReason,
59
69
  });
60
70
  return Object.freeze({
61
71
  maxProjectSessions: maxProjectSessions.value,
@@ -66,6 +76,7 @@ export function parseRuntimePolicy(environment) {
66
76
  compilerWorkerMode,
67
77
  compilerWorkerIdleTtlMs: compilerWorkerIdleTtlMs.value,
68
78
  denyApply,
79
+ projectStructuredContentAsText,
69
80
  reasons,
70
81
  });
71
82
  }
@@ -76,6 +87,46 @@ const WORKER_ENV_KEYS = [
76
87
  "AST_SYMBOL_INDEX_BUSY_TIMEOUT_MS",
77
88
  ];
78
89
  const MAX_WORKER_ENV_VALUE_LENGTH = 4096;
90
+ function parseClosedFixtureRecord(raw, expectedKeys) {
91
+ if (typeof raw !== "string" || raw.length > 1024 || raw.includes("\0"))
92
+ return undefined;
93
+ try {
94
+ const value = JSON.parse(raw);
95
+ if (!value || typeof value !== "object" || Array.isArray(value))
96
+ return undefined;
97
+ const record = value;
98
+ return Object.keys(record).sort().join(",") === expectedKeys ? record : undefined;
99
+ }
100
+ catch {
101
+ return undefined;
102
+ }
103
+ }
104
+ export function parseH03FixtureDescriptor(raw) {
105
+ const record = parseClosedFixtureRecord(raw, "controlDirectory,generation,nonce");
106
+ if (!record)
107
+ return undefined;
108
+ const { controlDirectory, nonce, generation } = record;
109
+ if (typeof controlDirectory !== "string" ||
110
+ controlDirectory.length > MAX_WORKER_ENV_VALUE_LENGTH ||
111
+ !path.isAbsolute(controlDirectory) ||
112
+ path.normalize(controlDirectory) !== controlDirectory ||
113
+ typeof nonce !== "string" ||
114
+ !/^[A-Za-z0-9_-]{16,64}$/.test(nonce) ||
115
+ !Number.isSafeInteger(generation) ||
116
+ generation < 1)
117
+ return undefined;
118
+ return Object.freeze({ controlDirectory, nonce, generation: generation });
119
+ }
120
+ export function parseH05FixtureDescriptor(raw) {
121
+ const record = parseClosedFixtureRecord(raw, "controlDirectory,generation,nonce,ownerToken");
122
+ if (!record)
123
+ return undefined;
124
+ const { ownerToken, ...baseValue } = record;
125
+ if (typeof ownerToken !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(ownerToken))
126
+ return undefined;
127
+ const base = parseH03FixtureDescriptor(JSON.stringify(baseValue));
128
+ return base ? Object.freeze({ ...base, ownerToken }) : undefined;
129
+ }
79
130
  function validEnvironmentString(value) {
80
131
  return (typeof value === "string" &&
81
132
  value.length <= MAX_WORKER_ENV_VALUE_LENGTH &&
@@ -100,6 +151,20 @@ export function createCompilerWorkerSpawnSpec(workerEntryPath, environment) {
100
151
  }
101
152
  projected[key] = value;
102
153
  }
154
+ const fixture = environment.AST_H03_FIXTURE;
155
+ if (fixture !== undefined) {
156
+ const descriptor = parseH03FixtureDescriptor(fixture);
157
+ if (!descriptor)
158
+ return { ok: false, reason: "invalid_environment" };
159
+ projected.AST_H03_FIXTURE = JSON.stringify(descriptor);
160
+ }
161
+ const lifecycleFixture = environment.AST_H05_FIXTURE;
162
+ if (lifecycleFixture !== undefined) {
163
+ const descriptor = parseH05FixtureDescriptor(lifecycleFixture);
164
+ if (!descriptor)
165
+ return { ok: false, reason: "invalid_environment" };
166
+ projected.AST_H05_FIXTURE = JSON.stringify(descriptor);
167
+ }
103
168
  const policy = parseRuntimePolicy(environment);
104
169
  Object.assign(projected, {
105
170
  AST_MAX_PROJECT_SESSIONS: String(policy.maxProjectSessions),
@@ -110,6 +175,7 @@ export function createCompilerWorkerSpawnSpec(workerEntryPath, environment) {
110
175
  // Forward the parent's resolved apply-guard policy so the supervised worker
111
176
  // registers the same tool surface (deny-by-default fail-closed, allow opt-in).
112
177
  AST_MCP_APPLY_GUARD: policy.denyApply ? "deny" : "allow",
178
+ ...(policy.projectStructuredContentAsText ? { AST_MCP_TEXT_PROJECTION: "canonical_json" } : {}),
113
179
  });
114
180
  return {
115
181
  ok: true,