@remnic/coding-graph 9.6.23 → 9.6.24

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.
@@ -4,8 +4,8 @@ import {
4
4
  executeAst,
5
5
  executeCypher,
6
6
  parseCypher
7
- } from "../chunk-5I2DBHOQ.js";
8
- import "../chunk-CPYJACC5.js";
7
+ } from "../chunk-7DI5P62Q.js";
8
+ import "../chunk-ZLCF3XQK.js";
9
9
  import "../chunk-ZVCMIM4T.js";
10
10
  export {
11
11
  CYPHER_LABEL_TO_DB_LABEL,
@@ -50,6 +50,23 @@ interface EdgeIR {
50
50
  readonly srcNodeId?: string;
51
51
  /** Optional content-derived destination node id — see {@link EdgeIR.srcNodeId}. */
52
52
  readonly dstNodeId?: string;
53
+ /**
54
+ * Repo-relative, extension-stripped path the dst must live in (issue
55
+ * #1894 review): derived from a relative import's module specifier. A
56
+ * hinted edge resolves ONLY among nodes whose file path matches the
57
+ * hint (`<hint>`, `<hint>.<ext>`, `<hint>/index.<ext>`, or
58
+ * `<hint>/__init__.<ext>`) — never via
59
+ * the global bare-name fallback — so `import { foo } from "./missing"`
60
+ * can never bind an unrelated same-named symbol elsewhere in the repo.
61
+ */
62
+ readonly dstPathHint?: string;
63
+ /**
64
+ * Language of the importing file (issue #1894 round 13): constrains the
65
+ * hinted dst's file extension to that language's module-resolution set
66
+ * so a polyglot repo cannot cross-bind a JS import to a same-named .py
67
+ * file.
68
+ */
69
+ readonly dstImporterLanguage?: string;
53
70
  }
54
71
  /**
55
72
  * Store input — the subset of @remnic/core's `FileIR` the store reads,
@@ -74,6 +91,16 @@ interface StoreFileIR {
74
91
  readonly symbols: readonly SymbolIR[];
75
92
  /** Store-specific edges derived from the IR by the caller. */
76
93
  readonly edges?: readonly EdgeIR[];
94
+ /**
95
+ * When present, the stale-edge delete in `upsertFileEdges` is scoped to
96
+ * edges whose provenance is in this list: prior src-owned edges of OTHER
97
+ * provenances survive un-asserted (issue #1891). The reindex pipeline
98
+ * asserts `["heuristic"]` because a fresh parse says nothing about
99
+ * `trace`/`lsp` edges; deleting them on every re-ingest would destroy
100
+ * state the parse never contradicted (rule 25). Absent = legacy
101
+ * behavior: every stale src-owned edge is deleted.
102
+ */
103
+ readonly assertedEdgeProvenances?: readonly EdgeProvenance[];
77
104
  /**
78
105
  * Per-file export list (mirrors core FileIR.exports). When present,
79
106
  * the write pipeline marks every node in this file whose `name`
@@ -4,7 +4,7 @@ import {
4
4
  GraphStore,
5
5
  MAX_TRAVERSE_PATHS_HOPS,
6
6
  nodeIdFor
7
- } from "./chunk-CPYJACC5.js";
7
+ } from "./chunk-ZLCF3XQK.js";
8
8
  import "./chunk-ZVCMIM4T.js";
9
9
  export {
10
10
  DEAD_CODE_EXCLUSION,
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-7DI5P62Q.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-ZLCF3XQK.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 {