@remnic/coding-graph 9.6.23 → 9.6.25

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/dist/index.d.ts CHANGED
@@ -1190,6 +1190,18 @@ interface ResolveOptions {
1190
1190
  * dstQualifiedName, type: "CALLS", confidence, provenance: "lsp"}`.
1191
1191
  */
1192
1192
  readonly applyUpgrades: (upgrades: readonly EdgeUpgrade[]) => Promise<void>;
1193
+ /**
1194
+ * Optional stale-edge reconciliation (issue #1895): after applying a
1195
+ * file batch's upgrades, the caller retires prior `lsp`-provenance
1196
+ * edges owned by that file whose `(src, dst, type)` keys the current
1197
+ * batch does NOT assert. When absent, stale lsp edges persist until
1198
+ * node pruning — the soft-fail path documented in #1894.
1199
+ */
1200
+ readonly reconcileLspEdges?: (filePath: string, assertedEdges: ReadonlyArray<{
1201
+ srcQualifiedName: string;
1202
+ dstQualifiedName: string;
1203
+ type: string;
1204
+ }>) => void;
1193
1205
  /**
1194
1206
  * Workspace root for resolving repo-relative file paths to absolute LSP
1195
1207
  * URIs and normalizing returned URIs back to repo-relative paths.
package/dist/index.js CHANGED
@@ -3,14 +3,14 @@ import {
3
3
  executeAst,
4
4
  executeCypher,
5
5
  parseCypher
6
- } from "./chunk-5I2DBHOQ.js";
6
+ } from "./chunk-I4R6GAAA.js";
7
7
  import {
8
8
  DEAD_CODE_EXCLUSION,
9
9
  DEFAULT_TRAVERSE_PATHS_MAX,
10
10
  GraphStore,
11
11
  MAX_TRAVERSE_PATHS_HOPS,
12
12
  nodeIdFor
13
- } from "./chunk-CPYJACC5.js";
13
+ } from "./chunk-ABDBWCXU.js";
14
14
  import {
15
15
  CODING_GRAPH_SCHEMA_VERSION,
16
16
  EDGE_PROVENANCE_VALUES,
@@ -175,15 +175,25 @@ var JS_FAMILY_DEFINITIONS = `
175
175
  var JS_IMPORTS = `
176
176
  (import_statement
177
177
  source: (string (string_fragment) @import.module)) @__import.stmt
178
+ ; Default and namespace imports bind LOCAL aliases whose exported symbol
179
+ ; is unknowable to Phase A (issue #1894 round 12): they appear in
180
+ ; importedNames (informational) but never in bindings (call-bindable).
178
181
  (import_statement
179
- (import_clause (identifier) @import.name)
182
+ (import_clause (identifier) @import.unboundName)
180
183
  source: (string (string_fragment) @import.module)) @__import.stmt
181
184
  (import_statement
182
- (import_clause (namespace_import (identifier) @import.name))
185
+ (import_clause (namespace_import (identifier) @import.unboundName))
183
186
  source: (string (string_fragment) @import.module)) @__import.stmt
184
187
  (import_statement
185
188
  (import_clause (named_imports (import_specifier name: (identifier) @import.name)))
186
189
  source: (string (string_fragment) @import.module)) @__import.stmt
190
+ ; Aliased named import (issue #1894 review): import { foo as bar } \u2014 the
191
+ ; local binding is the alias; the exported name stays the binding target.
192
+ (import_statement
193
+ (import_clause (named_imports (import_specifier
194
+ name: (identifier) @import.aliasExported
195
+ alias: (identifier) @import.aliasLocal)))
196
+ source: (string (string_fragment) @import.module)) @__import.stmt
187
197
 
188
198
  ; CommonJS require("...") \u2014 capture the module specifier so dependency
189
199
  ; edges exist for Node/CommonJS codebases, not just ES-module imports.
@@ -242,7 +252,10 @@ var JS_EXPORTS = `
242
252
  `.trim();
243
253
  var JS_CALLS = `
244
254
  (call_expression function: (identifier) @call.callee)
245
- (call_expression function: (member_expression property: (property_identifier) @call.callee))
255
+ ; Member calls are captured separately (issue #1894 review): obj.save()'s
256
+ ; \`save\` must never bind a bare visible symbol \u2014 method dispatch is
257
+ ; Phase B (LSP) territory. The emit layer marks these memberAccess: true.
258
+ (call_expression function: (member_expression property: (property_identifier) @call.member))
246
259
  `.trim();
247
260
  var JS_ROUTES = `
248
261
  ; Unified route pattern: captures verb + path + the arguments node.
@@ -290,6 +303,17 @@ var PYTHON_EXTRACTOR = {
290
303
  importsQuery: `
291
304
  (import_statement (dotted_name) @import.module) @__import.stmt
292
305
  (import_from_statement module_name: (dotted_name) @import.module) @__import.stmt
306
+ ; Imported names (issue #1894 review round 7): without these captures,
307
+ ; extractImports emits importedNames: [] and the heuristic resolver's
308
+ ; import bindings never fire for real Python parses.
309
+ (import_from_statement
310
+ module_name: (dotted_name) @import.module
311
+ name: (dotted_name) @import.name) @__import.stmt
312
+ (import_from_statement
313
+ module_name: (dotted_name) @import.module
314
+ name: (aliased_import
315
+ name: (dotted_name) @import.aliasExported
316
+ alias: (identifier) @import.aliasLocal)) @__import.stmt
293
317
  ; Python relative imports: from .models import User / from ..parent import X
294
318
  ; tree-sitter-python wraps the module inside a relative_import node, so the
295
319
  ; module_name field is NOT set. Capture the relative_import node itself so
@@ -297,11 +321,19 @@ var PYTHON_EXTRACTOR = {
297
321
  ; relative levels must not collapse to the same module name
298
322
  ; (chatgpt-codex-connector #1688 P2: 'Preserve dots in Python relative imports').
299
323
  (import_from_statement (relative_import) @import.module) @__import.stmt
324
+ (import_from_statement
325
+ (relative_import) @import.module
326
+ name: (dotted_name) @import.name) @__import.stmt
327
+ (import_from_statement
328
+ (relative_import) @import.module
329
+ name: (aliased_import
330
+ name: (dotted_name) @import.aliasExported
331
+ alias: (identifier) @import.aliasLocal)) @__import.stmt
300
332
  `.trim(),
301
333
  exportsQuery: ``,
302
334
  callSitesQuery: `
303
335
  (call function: (identifier) @call.callee)
304
- (call function: (attribute attribute: (identifier) @call.callee))
336
+ (call function: (attribute attribute: (identifier) @call.member))
305
337
  `.trim(),
306
338
  routesQuery: `
307
339
  (decorated_definition
@@ -337,7 +369,7 @@ var GO_EXTRACTOR = {
337
369
  exportsQuery: ``,
338
370
  callSitesQuery: `
339
371
  (call_expression function: (identifier) @call.callee)
340
- (call_expression function: (selector_expression field: (field_identifier) @call.callee))
372
+ (call_expression function: (selector_expression field: (field_identifier) @call.member))
341
373
  `.trim(),
342
374
  routesQuery: ``
343
375
  };
@@ -372,7 +404,7 @@ var RUST_EXTRACTOR = {
372
404
  exportsQuery: ``,
373
405
  callSitesQuery: `
374
406
  (call_expression function: (identifier) @call.callee)
375
- (call_expression function: (field_expression field: (field_identifier) @call.callee))
407
+ (call_expression function: (field_expression field: (field_identifier) @call.member))
376
408
  (call_expression function: (scoped_identifier) @call.callee)
377
409
  `.trim(),
378
410
  routesQuery: ``
@@ -391,7 +423,8 @@ var JAVA_EXTRACTOR = {
391
423
  `.trim(),
392
424
  exportsQuery: ``,
393
425
  callSitesQuery: `
394
- (method_invocation name: (identifier) @call.callee)
426
+ (method_invocation !object name: (identifier) @call.callee)
427
+ (method_invocation object: (_) name: (identifier) @call.member)
395
428
  `.trim(),
396
429
  routesQuery: ``
397
430
  };
@@ -429,7 +462,7 @@ var CPP_EXTRACTOR = {
429
462
  exportsQuery: ``,
430
463
  callSitesQuery: `
431
464
  (call_expression function: (identifier) @call.callee)
432
- (call_expression function: (field_expression field: (field_identifier) @call.callee))
465
+ (call_expression function: (field_expression field: (field_identifier) @call.member))
433
466
  `.trim(),
434
467
  routesQuery: ``
435
468
  };
@@ -448,7 +481,7 @@ var CSHARP_EXTRACTOR = {
448
481
  exportsQuery: ``,
449
482
  callSitesQuery: `
450
483
  (invocation_expression function: (identifier) @call.callee)
451
- (invocation_expression function: (member_access_expression name: (identifier) @call.callee))
484
+ (invocation_expression function: (member_access_expression name: (identifier) @call.member))
452
485
  `.trim(),
453
486
  routesQuery: ``
454
487
  };
@@ -467,7 +500,8 @@ var RUBY_EXTRACTOR = {
467
500
  `.trim(),
468
501
  exportsQuery: ``,
469
502
  callSitesQuery: `
470
- (call method: (identifier) @call.callee)
503
+ (call !receiver method: (identifier) @call.callee)
504
+ (call receiver: (_) method: (identifier) @call.member)
471
505
  `.trim(),
472
506
  routesQuery: ``
473
507
  };
@@ -485,7 +519,7 @@ var PHP_EXTRACTOR = {
485
519
  exportsQuery: ``,
486
520
  callSitesQuery: `
487
521
  (function_call_expression function: (name) @call.callee)
488
- (member_call_expression (name) @call.callee)
522
+ (member_call_expression (name) @call.member)
489
523
  `.trim(),
490
524
  routesQuery: ``
491
525
  };
@@ -691,11 +725,20 @@ function extractImports(root, language, lang) {
691
725
  let stmtStart = -1;
692
726
  let stmtEnd = -1;
693
727
  const names = [];
728
+ const unboundNames = [];
729
+ let aliasExported = "";
730
+ let aliasLocal = "";
694
731
  for (const cap of match.captures) {
695
732
  if (cap.name === "import.module") {
696
733
  moduleText = cleanModuleSpecifier(cap.node.text);
697
734
  } else if (cap.name === "import.name") {
698
735
  names.push(cap.node.text);
736
+ } else if (cap.name === "import.unboundName") {
737
+ unboundNames.push(cap.node.text);
738
+ } else if (cap.name === "import.aliasExported") {
739
+ aliasExported = cap.node.text;
740
+ } else if (cap.name === "import.aliasLocal") {
741
+ aliasLocal = cap.node.text;
699
742
  } else if (cap.name === "__import.stmt") {
700
743
  stmtStart = cap.node.startIndex;
701
744
  stmtEnd = cap.node.endIndex;
@@ -711,21 +754,34 @@ function extractImports(root, language, lang) {
711
754
  }
712
755
  }
713
756
  const key = `${stmtStart}:${moduleText}`;
714
- const existing = groups.get(key);
715
- if (existing) {
716
- for (const n of names) existing.names.add(n);
717
- } else {
718
- groups.set(key, {
757
+ let group = groups.get(key);
758
+ if (!group) {
759
+ group = {
719
760
  module: moduleText,
720
- names: new Set(names),
761
+ names: /* @__PURE__ */ new Set(),
762
+ bindings: /* @__PURE__ */ new Map(),
721
763
  startByte: stmtStart,
722
764
  endByte: stmtEnd
723
- });
765
+ };
766
+ groups.set(key, group);
767
+ }
768
+ for (const n of names) {
769
+ group.names.add(n);
770
+ if (!group.bindings.has(n)) group.bindings.set(n, n);
771
+ }
772
+ for (const n of unboundNames) {
773
+ group.names.add(n);
774
+ }
775
+ if (aliasExported && aliasLocal) {
776
+ group.names.add(aliasExported);
777
+ group.bindings.delete(aliasExported);
778
+ group.bindings.set(aliasLocal, aliasExported);
724
779
  }
725
780
  }
726
781
  return Array.from(groups.values()).map((g) => ({
727
782
  module: g.module,
728
783
  importedNames: Array.from(g.names).sort(),
784
+ bindings: Array.from(g.bindings.entries()).map(([local, exported]) => ({ exported, local })).sort((a, b) => a.local.localeCompare(b.local)),
729
785
  span: { startByte: g.startByte, endByte: g.endByte }
730
786
  })).sort((a, b) => a.span.startByte - b.span.startByte || a.module.localeCompare(b.module));
731
787
  } finally {
@@ -786,6 +842,12 @@ function extractCallSites(root, language, lang) {
786
842
  calleeNameCandidates: [cap.node.text],
787
843
  span: { startByte: cap.node.startIndex, endByte: cap.node.endIndex }
788
844
  });
845
+ } else if (cap.name === "call.member") {
846
+ callSites.push({
847
+ calleeNameCandidates: [cap.node.text],
848
+ memberAccess: true,
849
+ span: { startByte: cap.node.startIndex, endByte: cap.node.endIndex }
850
+ });
789
851
  }
790
852
  }
791
853
  return callSites.sort(
@@ -1101,6 +1163,219 @@ function createCodingGraphEngine(_options = {}) {
1101
1163
  // src/reindex.ts
1102
1164
  import { readFile as fsReadFile, realpath as fsRealpath } from "fs/promises";
1103
1165
  import path2 from "path";
1166
+
1167
+ // src/heuristic-resolution.ts
1168
+ import { posix } from "path";
1169
+ var EXPLICIT_RECEIVER_LANGUAGES = /* @__PURE__ */ new Set([
1170
+ "typescript",
1171
+ "tsx",
1172
+ "javascript",
1173
+ "python",
1174
+ // PHP method dispatch requires $this->/self:: — both arrive as
1175
+ // member_call_expression (memberAccess) — so a bare helper() inside a
1176
+ // method never means a sibling method (codex review round 12).
1177
+ "php"
1178
+ ]);
1179
+ var HEURISTIC_CONFIDENCE_SAME_FILE = 0.9;
1180
+ var HEURISTIC_CONFIDENCE_IMPORT_BOUND = 0.8;
1181
+ var HEURISTIC_PROVENANCE_SCOPE = ["heuristic"];
1182
+ function deriveHeuristicEdges(batch) {
1183
+ let callSites = 0;
1184
+ let resolved = 0;
1185
+ let skippedUnresolved = 0;
1186
+ let skippedAmbiguous = 0;
1187
+ let skippedNoEnclosingSymbol = 0;
1188
+ let skippedMemberAccess = 0;
1189
+ const files = [];
1190
+ for (const ir of batch) {
1191
+ const parentOf = /* @__PURE__ */ new Map();
1192
+ const kindOf = /* @__PURE__ */ new Map();
1193
+ for (const sym of ir.symbols) kindOf.set(sym.qualifiedName, sym.kind);
1194
+ for (const sym of ir.symbols) {
1195
+ let parent;
1196
+ for (const other of ir.symbols) {
1197
+ if (other === sym) continue;
1198
+ const contains = other.span.startByte <= sym.span.startByte && sym.span.endByte <= other.span.endByte;
1199
+ const identical = other.span.startByte === sym.span.startByte && other.span.endByte === sym.span.endByte;
1200
+ if (!contains || identical) continue;
1201
+ const size = other.span.endByte - other.span.startByte;
1202
+ if (!parent || size < parent.size) {
1203
+ parent = { qualifiedName: other.qualifiedName, size };
1204
+ }
1205
+ }
1206
+ parentOf.set(sym.qualifiedName, parent ? parent.qualifiedName : "");
1207
+ }
1208
+ const scopeByName = /* @__PURE__ */ new Map();
1209
+ for (const sym of ir.symbols) {
1210
+ const level = parentOf.get(sym.qualifiedName) ?? "";
1211
+ let names = scopeByName.get(level);
1212
+ if (!names) {
1213
+ names = /* @__PURE__ */ new Map();
1214
+ scopeByName.set(level, names);
1215
+ }
1216
+ const prior = names.get(sym.name);
1217
+ if (prior) {
1218
+ prior.count += 1;
1219
+ } else {
1220
+ names.set(sym.name, { qualifiedName: sym.qualifiedName, count: 1 });
1221
+ }
1222
+ }
1223
+ const importBindings = /* @__PURE__ */ new Map();
1224
+ for (const imp of ir.imports) {
1225
+ let specifier;
1226
+ if (imp.module.startsWith("./") || imp.module.startsWith("../")) {
1227
+ specifier = imp.module;
1228
+ } else if (ir.language === "python" && imp.module.startsWith(".")) {
1229
+ const dots = /^\.+/.exec(imp.module)?.[0].length ?? 1;
1230
+ const rest = imp.module.slice(dots).replace(/\./g, "/");
1231
+ specifier = `${"../".repeat(dots - 1)}${rest}` || ".";
1232
+ }
1233
+ if (specifier === void 0) continue;
1234
+ const joined = posix.join(posix.dirname(ir.path), specifier);
1235
+ const hint = joined.replace(/\/+$/, "").replace(/\.[cm]?[jt]sx?$/, "");
1236
+ if (hint.startsWith("../")) continue;
1237
+ const bindings = imp.bindings ?? imp.importedNames.map((name) => ({ exported: name, local: name }));
1238
+ let enclosing;
1239
+ for (const sym of ir.symbols) {
1240
+ if (sym.span.startByte > imp.span.startByte || imp.span.endByte > sym.span.endByte) continue;
1241
+ const size = sym.span.endByte - sym.span.startByte;
1242
+ if (!enclosing || size < enclosing.size) {
1243
+ enclosing = { qualifiedName: sym.qualifiedName, size };
1244
+ }
1245
+ }
1246
+ const enclosingKind = enclosing === void 0 ? void 0 : kindOf.get(enclosing.qualifiedName);
1247
+ const visible = enclosingKind === void 0 || enclosingKind !== "class" && enclosingKind !== "interface" && enclosingKind !== "enum";
1248
+ for (const b of bindings) {
1249
+ const list = importBindings.get(b.local) ?? [];
1250
+ list.push({
1251
+ exported: b.exported,
1252
+ hint,
1253
+ enclosing: visible ? enclosing?.qualifiedName : void 0,
1254
+ // A class-body import is genuinely invisible: store it with a
1255
+ // sentinel that can never match an ancestor chain.
1256
+ invisible: !visible
1257
+ });
1258
+ importBindings.set(b.local, list);
1259
+ }
1260
+ }
1261
+ const edges = [];
1262
+ const seenKeys = /* @__PURE__ */ new Set();
1263
+ for (const site of ir.callSites) {
1264
+ callSites += 1;
1265
+ if (site.memberAccess === true) {
1266
+ skippedMemberAccess += 1;
1267
+ continue;
1268
+ }
1269
+ let src;
1270
+ for (const sym of ir.symbols) {
1271
+ if (sym.span.startByte > site.span.startByte || site.span.endByte > sym.span.endByte) continue;
1272
+ const size = sym.span.endByte - sym.span.startByte;
1273
+ if (!src || size < src.size) {
1274
+ src = { qualifiedName: sym.qualifiedName, size };
1275
+ }
1276
+ }
1277
+ if (!src) {
1278
+ skippedNoEnclosingSymbol += 1;
1279
+ continue;
1280
+ }
1281
+ const excludeClassScopes = EXPLICIT_RECEIVER_LANGUAGES.has(ir.language);
1282
+ const scopeLevels = [src.qualifiedName];
1283
+ const ancestorChain = /* @__PURE__ */ new Set([src.qualifiedName]);
1284
+ let cursor = src.qualifiedName;
1285
+ while (cursor !== void 0 && cursor !== "") {
1286
+ cursor = parentOf.get(cursor) ?? "";
1287
+ if (cursor !== "") ancestorChain.add(cursor);
1288
+ if (excludeClassScopes) {
1289
+ const kind = cursor === "" ? void 0 : kindOf.get(cursor);
1290
+ if (kind === "class" || kind === "interface" || kind === "enum") continue;
1291
+ }
1292
+ scopeLevels.push(cursor);
1293
+ }
1294
+ let dstQualifiedName;
1295
+ let dstPathHint;
1296
+ let confidence = 0;
1297
+ let sawAmbiguous = false;
1298
+ for (const candidate of site.calleeNameCandidates) {
1299
+ let candidateAmbiguous = false;
1300
+ for (const level of scopeLevels) {
1301
+ const match = scopeByName.get(level)?.get(candidate);
1302
+ if (!match) continue;
1303
+ if (match.count === 1) {
1304
+ dstQualifiedName = match.qualifiedName;
1305
+ confidence = HEURISTIC_CONFIDENCE_SAME_FILE;
1306
+ } else {
1307
+ candidateAmbiguous = true;
1308
+ }
1309
+ break;
1310
+ }
1311
+ if (dstQualifiedName !== void 0) break;
1312
+ if (candidateAmbiguous) {
1313
+ sawAmbiguous = true;
1314
+ continue;
1315
+ }
1316
+ const candidates = importBindings.get(candidate);
1317
+ if (candidates !== void 0) {
1318
+ let chosen;
1319
+ let chosenDepth = -1;
1320
+ for (const c of candidates) {
1321
+ if (c.invisible) continue;
1322
+ if (c.enclosing === void 0) {
1323
+ if (chosenDepth < 0) {
1324
+ chosen = c;
1325
+ chosenDepth = 0;
1326
+ }
1327
+ continue;
1328
+ }
1329
+ if (ancestorChain.has(c.enclosing)) {
1330
+ const depth = [...ancestorChain].indexOf(c.enclosing);
1331
+ if (chosenDepth < 0 || depth < chosenDepth) {
1332
+ chosen = c;
1333
+ chosenDepth = depth;
1334
+ }
1335
+ }
1336
+ }
1337
+ if (chosen) {
1338
+ dstQualifiedName = chosen.exported;
1339
+ dstPathHint = chosen.hint;
1340
+ confidence = HEURISTIC_CONFIDENCE_IMPORT_BOUND;
1341
+ break;
1342
+ }
1343
+ }
1344
+ }
1345
+ if (dstQualifiedName === void 0) {
1346
+ if (sawAmbiguous) skippedAmbiguous += 1;
1347
+ else skippedUnresolved += 1;
1348
+ continue;
1349
+ }
1350
+ resolved += 1;
1351
+ const key = `${src.qualifiedName}\0${dstQualifiedName}\0${dstPathHint ?? ""}`;
1352
+ if (seenKeys.has(key)) continue;
1353
+ seenKeys.add(key);
1354
+ edges.push({
1355
+ srcQualifiedName: src.qualifiedName,
1356
+ dstQualifiedName,
1357
+ type: "CALLS",
1358
+ confidence,
1359
+ provenance: "heuristic",
1360
+ ...dstPathHint !== void 0 ? { dstPathHint, dstImporterLanguage: ir.language } : {}
1361
+ });
1362
+ }
1363
+ files.push({ ...ir, edges });
1364
+ }
1365
+ return {
1366
+ files,
1367
+ stats: {
1368
+ callSites,
1369
+ resolved,
1370
+ skippedUnresolved,
1371
+ skippedAmbiguous,
1372
+ skippedNoEnclosingSymbol,
1373
+ skippedMemberAccess
1374
+ }
1375
+ };
1376
+ }
1377
+
1378
+ // src/reindex.ts
1104
1379
  var META_KEY_LAST_HEAD = "last_indexed_head";
1105
1380
  var META_KEY_PENDING_PARSE_FAILURES = "pending_parse_failures";
1106
1381
  function planReindex(lastState, facts) {
@@ -1467,7 +1742,7 @@ function computeNextPending(args) {
1467
1742
  return [...next];
1468
1743
  }
1469
1744
  async function ingestFiles(store, repoRoot, parseFile, readFile2, paths, deletePaths = []) {
1470
- const batch = [];
1745
+ const parsed = [];
1471
1746
  const parseFailedPaths = [];
1472
1747
  for (const relPath of paths) {
1473
1748
  if (!isCanonicalRelativePath(relPath)) {
@@ -1488,8 +1763,13 @@ async function ingestFiles(store, repoRoot, parseFile, readFile2, paths, deleteP
1488
1763
  parseFailedPaths.push(relPath);
1489
1764
  continue;
1490
1765
  }
1491
- batch.push(parseResult.ir);
1766
+ parsed.push(parseResult.ir);
1492
1767
  }
1768
+ const derived = deriveHeuristicEdges(parsed);
1769
+ const batch = derived.files.map((file) => ({
1770
+ ...file,
1771
+ assertedEdgeProvenances: HEURISTIC_PROVENANCE_SCOPE
1772
+ }));
1493
1773
  const upsertResult = await store.upsertFileBatch(batch, deletePaths);
1494
1774
  if (!upsertResult.ok) {
1495
1775
  return {
@@ -2667,6 +2947,7 @@ async function executeLspResolution(requests, options) {
2667
2947
  for (const [filePath, batchReqs] of byFile) {
2668
2948
  const upgrades = [];
2669
2949
  let batchFailed = false;
2950
+ let batchExhaustive = true;
2670
2951
  const firstReq = batchReqs[0];
2671
2952
  client.didOpen({
2672
2953
  uri: filePathToUri(filePath, options.workspaceRoot),
@@ -2691,6 +2972,7 @@ async function executeLspResolution(requests, options) {
2691
2972
  batchFailed = true;
2692
2973
  break;
2693
2974
  }
2975
+ batchExhaustive = false;
2694
2976
  unresolved++;
2695
2977
  continue;
2696
2978
  }
@@ -2720,13 +3002,23 @@ async function executeLspResolution(requests, options) {
2720
3002
  unresolved += upgrades.length;
2721
3003
  break;
2722
3004
  }
2723
- if (upgrades.length > 0) {
2724
- try {
3005
+ try {
3006
+ if (upgrades.length > 0) {
2725
3007
  await applyUpgrades(upgrades);
2726
3008
  upgraded += upgrades.length;
2727
- } catch {
2728
- unresolved += upgrades.length;
2729
3009
  }
3010
+ if (batchExhaustive && options.reconcileLspEdges) {
3011
+ options.reconcileLspEdges(
3012
+ filePath,
3013
+ upgrades.map((u) => ({
3014
+ srcQualifiedName: u.srcQualifiedName,
3015
+ dstQualifiedName: u.dstQualifiedName,
3016
+ type: u.type
3017
+ }))
3018
+ );
3019
+ }
3020
+ } catch {
3021
+ unresolved += upgrades.length;
2730
3022
  }
2731
3023
  }
2732
3024
  return {