homegraph 1.2.0 → 1.4.1

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 (284) hide show
  1. package/dist/bin/homegraph.js +3 -0
  2. package/dist/bin/homegraph.js.map +1 -1
  3. package/dist/context/index.d.ts.map +1 -1
  4. package/dist/context/index.js +85 -0
  5. package/dist/context/index.js.map +1 -1
  6. package/dist/db/index.d.ts +64 -5
  7. package/dist/db/index.d.ts.map +1 -1
  8. package/dist/db/index.js +178 -14
  9. package/dist/db/index.js.map +1 -1
  10. package/dist/db/migrations.d.ts +1 -1
  11. package/dist/db/migrations.d.ts.map +1 -1
  12. package/dist/db/migrations.js +19 -1
  13. package/dist/db/migrations.js.map +1 -1
  14. package/dist/db/queries.d.ts +117 -2
  15. package/dist/db/queries.d.ts.map +1 -1
  16. package/dist/db/queries.js +299 -6
  17. package/dist/db/queries.js.map +1 -1
  18. package/dist/db/schema.sql +32 -1
  19. package/dist/db/wal-valve.d.ts +106 -0
  20. package/dist/db/wal-valve.d.ts.map +1 -0
  21. package/dist/db/wal-valve.js +208 -0
  22. package/dist/db/wal-valve.js.map +1 -0
  23. package/dist/directory.d.ts +9 -5
  24. package/dist/directory.d.ts.map +1 -1
  25. package/dist/directory.js +210 -19
  26. package/dist/directory.js.map +1 -1
  27. package/dist/extraction/cfml-extractor.d.ts +107 -0
  28. package/dist/extraction/cfml-extractor.d.ts.map +1 -0
  29. package/dist/extraction/cfml-extractor.js +494 -0
  30. package/dist/extraction/cfml-extractor.js.map +1 -0
  31. package/dist/extraction/grammars.d.ts +25 -1
  32. package/dist/extraction/grammars.d.ts.map +1 -1
  33. package/dist/extraction/grammars.js +177 -24
  34. package/dist/extraction/grammars.js.map +1 -1
  35. package/dist/extraction/index.d.ts +43 -4
  36. package/dist/extraction/index.d.ts.map +1 -1
  37. package/dist/extraction/index.js +308 -5
  38. package/dist/extraction/index.js.map +1 -1
  39. package/dist/extraction/languages/c-cpp.d.ts +42 -0
  40. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  41. package/dist/extraction/languages/c-cpp.js +369 -4
  42. package/dist/extraction/languages/c-cpp.js.map +1 -1
  43. package/dist/extraction/languages/cfquery.d.ts +12 -0
  44. package/dist/extraction/languages/cfquery.d.ts.map +1 -0
  45. package/dist/extraction/languages/cfquery.js +28 -0
  46. package/dist/extraction/languages/cfquery.js.map +1 -0
  47. package/dist/extraction/languages/cfscript.d.ts +3 -0
  48. package/dist/extraction/languages/cfscript.d.ts.map +1 -0
  49. package/dist/extraction/languages/cfscript.js +73 -0
  50. package/dist/extraction/languages/cfscript.js.map +1 -0
  51. package/dist/extraction/languages/cobol.d.ts +33 -0
  52. package/dist/extraction/languages/cobol.d.ts.map +1 -0
  53. package/dist/extraction/languages/cobol.js +499 -0
  54. package/dist/extraction/languages/cobol.js.map +1 -0
  55. package/dist/extraction/languages/erlang.d.ts +3 -0
  56. package/dist/extraction/languages/erlang.d.ts.map +1 -0
  57. package/dist/extraction/languages/erlang.js +350 -0
  58. package/dist/extraction/languages/erlang.js.map +1 -0
  59. package/dist/extraction/languages/index.d.ts.map +1 -1
  60. package/dist/extraction/languages/index.js +16 -0
  61. package/dist/extraction/languages/index.js.map +1 -1
  62. package/dist/extraction/languages/nix.d.ts +3 -0
  63. package/dist/extraction/languages/nix.d.ts.map +1 -0
  64. package/dist/extraction/languages/nix.js +294 -0
  65. package/dist/extraction/languages/nix.js.map +1 -0
  66. package/dist/extraction/languages/solidity.d.ts +3 -0
  67. package/dist/extraction/languages/solidity.d.ts.map +1 -0
  68. package/dist/extraction/languages/solidity.js +293 -0
  69. package/dist/extraction/languages/solidity.js.map +1 -0
  70. package/dist/extraction/languages/terraform.d.ts +3 -0
  71. package/dist/extraction/languages/terraform.d.ts.map +1 -0
  72. package/dist/extraction/languages/terraform.js +641 -0
  73. package/dist/extraction/languages/terraform.js.map +1 -0
  74. package/dist/extraction/languages/vbnet.d.ts +11 -0
  75. package/dist/extraction/languages/vbnet.d.ts.map +1 -0
  76. package/dist/extraction/languages/vbnet.js +141 -0
  77. package/dist/extraction/languages/vbnet.js.map +1 -0
  78. package/dist/extraction/mybatis-extractor.d.ts +30 -10
  79. package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  80. package/dist/extraction/mybatis-extractor.js +140 -38
  81. package/dist/extraction/mybatis-extractor.js.map +1 -1
  82. package/dist/extraction/parse-pool.d.ts +27 -1
  83. package/dist/extraction/parse-pool.d.ts.map +1 -1
  84. package/dist/extraction/parse-pool.js +75 -7
  85. package/dist/extraction/parse-pool.js.map +1 -1
  86. package/dist/extraction/parse-worker.js +9 -2
  87. package/dist/extraction/parse-worker.js.map +1 -1
  88. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  89. package/dist/extraction/tree-sitter-helpers.js +1 -0
  90. package/dist/extraction/tree-sitter-helpers.js.map +1 -1
  91. package/dist/extraction/tree-sitter-types.d.ts +3 -1
  92. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  93. package/dist/extraction/tree-sitter.d.ts +38 -0
  94. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  95. package/dist/extraction/tree-sitter.js +754 -10
  96. package/dist/extraction/tree-sitter.js.map +1 -1
  97. package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  98. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  99. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  100. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  101. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  102. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  103. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  104. package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  105. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  106. package/dist/graph/traversal.d.ts.map +1 -1
  107. package/dist/graph/traversal.js +1 -1
  108. package/dist/graph/traversal.js.map +1 -1
  109. package/dist/index.d.ts +60 -2
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +348 -5
  112. package/dist/index.js.map +1 -1
  113. package/dist/installer/index.d.ts +42 -0
  114. package/dist/installer/index.d.ts.map +1 -1
  115. package/dist/installer/index.js +97 -2
  116. package/dist/installer/index.js.map +1 -1
  117. package/dist/mcp/daemon.d.ts +25 -3
  118. package/dist/mcp/daemon.d.ts.map +1 -1
  119. package/dist/mcp/daemon.js +62 -8
  120. package/dist/mcp/daemon.js.map +1 -1
  121. package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  122. package/dist/mcp/dynamic-boundaries.js +2 -1
  123. package/dist/mcp/dynamic-boundaries.js.map +1 -1
  124. package/dist/mcp/early-ppid.d.ts +26 -0
  125. package/dist/mcp/early-ppid.d.ts.map +1 -0
  126. package/dist/mcp/early-ppid.js +29 -0
  127. package/dist/mcp/early-ppid.js.map +1 -0
  128. package/dist/mcp/engine.d.ts +4 -5
  129. package/dist/mcp/engine.d.ts.map +1 -1
  130. package/dist/mcp/engine.js +27 -1
  131. package/dist/mcp/engine.js.map +1 -1
  132. package/dist/mcp/index.d.ts.map +1 -1
  133. package/dist/mcp/index.js +41 -6
  134. package/dist/mcp/index.js.map +1 -1
  135. package/dist/mcp/liveness-watchdog.d.ts +18 -1
  136. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  137. package/dist/mcp/liveness-watchdog.js +73 -13
  138. package/dist/mcp/liveness-watchdog.js.map +1 -1
  139. package/dist/mcp/memory-budget.d.ts +30 -0
  140. package/dist/mcp/memory-budget.d.ts.map +1 -0
  141. package/dist/mcp/memory-budget.js +123 -0
  142. package/dist/mcp/memory-budget.js.map +1 -0
  143. package/dist/mcp/proxy.d.ts.map +1 -1
  144. package/dist/mcp/proxy.js +88 -4
  145. package/dist/mcp/proxy.js.map +1 -1
  146. package/dist/mcp/query-cache.d.ts +35 -4
  147. package/dist/mcp/query-cache.d.ts.map +1 -1
  148. package/dist/mcp/query-cache.js +89 -19
  149. package/dist/mcp/query-cache.js.map +1 -1
  150. package/dist/mcp/query-pool.d.ts +46 -12
  151. package/dist/mcp/query-pool.d.ts.map +1 -1
  152. package/dist/mcp/query-pool.js +100 -23
  153. package/dist/mcp/query-pool.js.map +1 -1
  154. package/dist/mcp/server-instructions.d.ts +4 -27
  155. package/dist/mcp/server-instructions.d.ts.map +1 -1
  156. package/dist/mcp/server-instructions.js +39 -84
  157. package/dist/mcp/server-instructions.js.map +1 -1
  158. package/dist/mcp/session.d.ts +14 -0
  159. package/dist/mcp/session.d.ts.map +1 -1
  160. package/dist/mcp/session.js +28 -1
  161. package/dist/mcp/session.js.map +1 -1
  162. package/dist/mcp/startup-handshake.d.ts +44 -0
  163. package/dist/mcp/startup-handshake.d.ts.map +1 -0
  164. package/dist/mcp/startup-handshake.js +73 -0
  165. package/dist/mcp/startup-handshake.js.map +1 -0
  166. package/dist/mcp/tools.d.ts +104 -4
  167. package/dist/mcp/tools.d.ts.map +1 -1
  168. package/dist/mcp/tools.js +2390 -191
  169. package/dist/mcp/tools.js.map +1 -1
  170. package/dist/mcp/transport.d.ts.map +1 -1
  171. package/dist/mcp/transport.js +18 -0
  172. package/dist/mcp/transport.js.map +1 -1
  173. package/dist/project-config.d.ts +38 -0
  174. package/dist/project-config.d.ts.map +1 -1
  175. package/dist/project-config.js +101 -2
  176. package/dist/project-config.js.map +1 -1
  177. package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  178. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  179. package/dist/resolution/c-fnptr-synthesizer.js +178 -146
  180. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  181. package/dist/resolution/callback-synthesizer.d.ts +9 -1
  182. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  183. package/dist/resolution/callback-synthesizer.js +1164 -176
  184. package/dist/resolution/callback-synthesizer.js.map +1 -1
  185. package/dist/resolution/frameworks/cics.d.ts +20 -0
  186. package/dist/resolution/frameworks/cics.d.ts.map +1 -0
  187. package/dist/resolution/frameworks/cics.js +90 -0
  188. package/dist/resolution/frameworks/cics.js.map +1 -0
  189. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  190. package/dist/resolution/frameworks/index.js +3 -0
  191. package/dist/resolution/frameworks/index.js.map +1 -1
  192. package/dist/resolution/frameworks/java.d.ts.map +1 -1
  193. package/dist/resolution/frameworks/java.js +14 -6
  194. package/dist/resolution/frameworks/java.js.map +1 -1
  195. package/dist/resolution/frameworks/terraform.d.ts +38 -0
  196. package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  197. package/dist/resolution/frameworks/terraform.js +277 -0
  198. package/dist/resolution/frameworks/terraform.js.map +1 -0
  199. package/dist/resolution/goframe-synthesizer.d.ts +2 -1
  200. package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  201. package/dist/resolution/goframe-synthesizer.js +8 -3
  202. package/dist/resolution/goframe-synthesizer.js.map +1 -1
  203. package/dist/resolution/import-resolver.d.ts +7 -0
  204. package/dist/resolution/import-resolver.d.ts.map +1 -1
  205. package/dist/resolution/import-resolver.js +129 -4
  206. package/dist/resolution/import-resolver.js.map +1 -1
  207. package/dist/resolution/index.d.ts +60 -7
  208. package/dist/resolution/index.d.ts.map +1 -1
  209. package/dist/resolution/index.js +430 -80
  210. package/dist/resolution/index.js.map +1 -1
  211. package/dist/resolution/name-matcher.d.ts +0 -3
  212. package/dist/resolution/name-matcher.d.ts.map +1 -1
  213. package/dist/resolution/name-matcher.js +354 -33
  214. package/dist/resolution/name-matcher.js.map +1 -1
  215. package/dist/resolution/strip-comments.d.ts +1 -1
  216. package/dist/resolution/strip-comments.d.ts.map +1 -1
  217. package/dist/resolution/strip-comments.js +49 -0
  218. package/dist/resolution/strip-comments.js.map +1 -1
  219. package/dist/resolution/types.d.ts +29 -0
  220. package/dist/resolution/types.d.ts.map +1 -1
  221. package/dist/resolution/workspace-packages.d.ts +10 -0
  222. package/dist/resolution/workspace-packages.d.ts.map +1 -1
  223. package/dist/resolution/workspace-packages.js +142 -4
  224. package/dist/resolution/workspace-packages.js.map +1 -1
  225. package/dist/search/identifier-segments.d.ts +60 -0
  226. package/dist/search/identifier-segments.d.ts.map +1 -0
  227. package/dist/search/identifier-segments.js +176 -0
  228. package/dist/search/identifier-segments.js.map +1 -0
  229. package/dist/search/query-utils.d.ts +201 -0
  230. package/dist/search/query-utils.d.ts.map +1 -1
  231. package/dist/search/query-utils.js +919 -2
  232. package/dist/search/query-utils.js.map +1 -1
  233. package/dist/sync/git-hooks.d.ts.map +1 -1
  234. package/dist/sync/git-hooks.js +2 -0
  235. package/dist/sync/git-hooks.js.map +1 -1
  236. package/dist/sync/watcher.d.ts +10 -5
  237. package/dist/sync/watcher.d.ts.map +1 -1
  238. package/dist/sync/watcher.js +51 -14
  239. package/dist/sync/watcher.js.map +1 -1
  240. package/dist/sync/worktree.d.ts.map +1 -1
  241. package/dist/sync/worktree.js +5 -0
  242. package/dist/sync/worktree.js.map +1 -1
  243. package/dist/types.d.ts +19 -1
  244. package/dist/types.d.ts.map +1 -1
  245. package/dist/types.js +10 -0
  246. package/dist/types.js.map +1 -1
  247. package/dist/upgrade/index.d.ts +32 -0
  248. package/dist/upgrade/index.d.ts.map +1 -1
  249. package/dist/upgrade/index.js +162 -14
  250. package/dist/upgrade/index.js.map +1 -1
  251. package/dist/upgrade/remove-binary.d.ts +87 -0
  252. package/dist/upgrade/remove-binary.d.ts.map +1 -0
  253. package/dist/upgrade/remove-binary.js +289 -0
  254. package/dist/upgrade/remove-binary.js.map +1 -0
  255. package/dist/upgrade/update-check.d.ts +92 -0
  256. package/dist/upgrade/update-check.d.ts.map +1 -0
  257. package/dist/upgrade/update-check.js +258 -0
  258. package/dist/upgrade/update-check.js.map +1 -0
  259. package/package.json +2 -2
  260. package/scripts/agent-eval/run-all.sh +6 -0
  261. package/scripts/build-bundle.sh +5 -0
  262. package/scripts/npm-shim.js +8 -1
  263. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  264. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  265. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  266. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  267. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  268. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  269. package/scripts/exp_boundary_eval/data/agents.json +0 -109
  270. package/scripts/exp_boundary_eval/data/experiments.json +0 -140
  271. package/scripts/qa_eval/README.md +0 -407
  272. package/scripts/qa_eval/_test_deveco_probe.py +0 -41
  273. package/scripts/qa_eval/agent_runner.py +0 -526
  274. package/scripts/qa_eval/data/.gitignore +0 -4
  275. package/scripts/qa_eval/data/test-set.jsonl +0 -2
  276. package/scripts/qa_eval/eval_metrics.py +0 -274
  277. package/scripts/qa_eval/external_agent.py +0 -976
  278. package/scripts/qa_eval/llm_config.py +0 -92
  279. package/scripts/qa_eval/memory_monitor.py +0 -132
  280. package/scripts/qa_eval/my_answer_accuracy.py +0 -187
  281. package/scripts/qa_eval/requirements.txt +0 -2
  282. package/scripts/qa_eval/run_pipeline.py +0 -804
  283. package/scripts/qa_eval/stats_efficiency.py +0 -279
  284. package/scripts/qa_eval/stats_scores.py +0 -207
@@ -68,6 +68,8 @@ const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'g
68
68
  const SCOPED_CHAIN_LANGUAGES = new Set(['rust']);
69
69
  /** The extractor's chained-receiver encoding: `<inner>().<method>`. */
70
70
  const CHAIN_SHAPE = /^(.+)\(\)\.(\w+)$/;
71
+ /** PHP `$this->prop->method()` encoded as `this->prop.method` — no `()`, so CHAIN_SHAPE misses it. */
72
+ const PHP_PROP_SHAPE = /^this->\w+\.\w+$/;
71
73
  /**
72
74
  * Cache size limits. Each per-resolver cache is bounded so memory
73
75
  * stays flat on large codebases (20k+ files). Sizes were chosen to
@@ -240,6 +242,17 @@ class ReferenceResolver {
240
242
  nameCache; // name → nodes cache
241
243
  lowerNameCache; // lower(name) → nodes cache
242
244
  qualifiedNameCache; // qualified_name → nodes cache
245
+ fileLinesCache; // file → split lines cache
246
+ methodMatchCache; // lang\0Type::method → matching method nodes
247
+ // Node kinds are a small fixed set (~24), so this is a plain Map, not an LRU.
248
+ // getNodesByKind returns the FULL node list for a kind; it was previously
249
+ // uncached — a per-ref `SELECT * FROM nodes WHERE kind=?` + row-mapping. Called
250
+ // for every dotted call ref by the Spring resolver (constants) and every
251
+ // `hook_` ref by the Drupal resolver (functions), that scan dominated
252
+ // resolution on large repos (#1180). The node set is stable within a
253
+ // resolution pass (same lifetime assumption as nameCache); clearCaches() resets
254
+ // it between passes. Callers must treat the returned array as read-only.
255
+ nodesByKindCache = new Map();
243
256
  knownNames = null; // all known symbol names for fast pre-filtering
244
257
  knownFiles = null;
245
258
  cachesWarmed = false;
@@ -265,6 +278,10 @@ class ReferenceResolver {
265
278
  this.nameCache = new lru_cache_1.LRUCache(limit);
266
279
  this.lowerNameCache = new lru_cache_1.LRUCache(limit);
267
280
  this.qualifiedNameCache = new lru_cache_1.LRUCache(limit);
281
+ // Split-lines arrays are heavier than content strings; refs arrive
282
+ // file-ordered, so a small cache still hits nearly always.
283
+ this.fileLinesCache = new lru_cache_1.LRUCache(contentLimit);
284
+ this.methodMatchCache = new lru_cache_1.LRUCache(limit);
268
285
  this.context = this.createContext();
269
286
  }
270
287
  /**
@@ -320,6 +337,28 @@ class ReferenceResolver {
320
337
  this.knownNames = new Set(this.queries.getAllNodeNames());
321
338
  this.cachesWarmed = true;
322
339
  }
340
+ /**
341
+ * warmCaches for the async resolution entry points: streams the distinct
342
+ * name set with periodic yields instead of one synchronous `.all()`. On a
343
+ * multi-million-node index the DISTINCT scan is a solid multi-second block
344
+ * (measured up to 28s inside `homegraph sync` on the Linux kernel index),
345
+ * long enough to matter to the #850 watchdog on slower hardware. Same
346
+ * result, same memory — only the event loop keeps turning.
347
+ */
348
+ async warmCachesYielding(onYield) {
349
+ if (this.cachesWarmed)
350
+ return;
351
+ this.knownFiles = new Set(this.queries.getAllFilePaths());
352
+ const names = new Set();
353
+ let scanned = 0;
354
+ for (const name of this.queries.iterateNodeNames()) {
355
+ names.add(name);
356
+ if ((++scanned & 8191) === 0)
357
+ await onYield();
358
+ }
359
+ this.knownNames = names;
360
+ this.cachesWarmed = true;
361
+ }
323
362
  /**
324
363
  * Clear internal caches
325
364
  */
@@ -331,10 +370,30 @@ class ReferenceResolver {
331
370
  this.nameCache.clear();
332
371
  this.lowerNameCache.clear();
333
372
  this.qualifiedNameCache.clear();
373
+ this.fileLinesCache.clear();
374
+ this.methodMatchCache.clear();
375
+ this.nodesByKindCache.clear();
334
376
  this.knownNames = null;
335
377
  this.knownFiles = null;
336
378
  this.cachesWarmed = false;
337
379
  }
380
+ /** `readFile` through the LRU content cache (null = read failed, also cached). */
381
+ readFileCached(filePath) {
382
+ if (this.fileCache.has(filePath)) {
383
+ return this.fileCache.get(filePath);
384
+ }
385
+ const fullPath = path.join(this.projectRoot, filePath);
386
+ try {
387
+ const content = fs.readFileSync(fullPath, 'utf-8');
388
+ this.fileCache.set(filePath, content);
389
+ return content;
390
+ }
391
+ catch (error) {
392
+ (0, errors_1.logDebug)('Failed to read file for resolution', { filePath, error: String(error) });
393
+ this.fileCache.set(filePath, null);
394
+ return null;
395
+ }
396
+ }
338
397
  /**
339
398
  * Create the resolution context
340
399
  */
@@ -354,6 +413,30 @@ class ReferenceResolver {
354
413
  this.nameCache.set(name, result);
355
414
  return result;
356
415
  },
416
+ getMethodMatches: (typeName, methodName, language) => {
417
+ const key = `${language} ${typeName}::${methodName}`;
418
+ const cached = this.methodMatchCache.get(key);
419
+ if (cached !== undefined)
420
+ return cached;
421
+ let candidates = this.nameCache.get(methodName);
422
+ if (candidates === undefined) {
423
+ candidates = this.queries.getNodesByName(methodName);
424
+ this.nameCache.set(methodName, candidates);
425
+ }
426
+ const want = `${typeName}::${methodName}`;
427
+ const matches = [];
428
+ for (const m of candidates) {
429
+ if (m.kind !== 'method')
430
+ continue;
431
+ if (m.language !== language)
432
+ continue;
433
+ const qn = m.qualifiedName;
434
+ if (qn === want || qn.endsWith(`::${want}`))
435
+ matches.push(m);
436
+ }
437
+ this.methodMatchCache.set(key, matches);
438
+ return matches;
439
+ },
357
440
  getNodesByQualifiedName: (qualifiedName) => {
358
441
  const cached = this.qualifiedNameCache.get(qualifiedName);
359
442
  if (cached !== undefined)
@@ -363,8 +446,18 @@ class ReferenceResolver {
363
446
  return result;
364
447
  },
365
448
  getNodesByKind: (kind) => {
366
- return this.queries.getNodesByKind(kind);
449
+ const cached = this.nodesByKindCache.get(kind);
450
+ if (cached !== undefined)
451
+ return cached;
452
+ const result = this.queries.getNodesByKind(kind);
453
+ this.nodesByKindCache.set(kind, result);
454
+ return result;
367
455
  },
456
+ // Streamed, uncached — synthesizers scan-and-filter whole kinds, and
457
+ // both the materialized array AND the per-kind cache retention are
458
+ // O(nodes) memory (#1212). Per-ref resolvers keep the cached array
459
+ // variant above.
460
+ iterateNodesByKind: (kind) => this.queries.iterateNodesByKind(kind),
368
461
  fileExists: (filePath) => {
369
462
  // Check pre-built known files set first (O(1))
370
463
  if (this.knownFiles) {
@@ -383,21 +476,15 @@ class ReferenceResolver {
383
476
  return false;
384
477
  }
385
478
  },
386
- readFile: (filePath) => {
387
- if (this.fileCache.has(filePath)) {
388
- return this.fileCache.get(filePath);
389
- }
390
- const fullPath = path.join(this.projectRoot, filePath);
391
- try {
392
- const content = fs.readFileSync(fullPath, 'utf-8');
393
- this.fileCache.set(filePath, content);
394
- return content;
395
- }
396
- catch (error) {
397
- (0, errors_1.logDebug)('Failed to read file for resolution', { filePath, error: String(error) });
398
- this.fileCache.set(filePath, null);
399
- return null;
400
- }
479
+ readFile: (filePath) => this.readFileCached(filePath),
480
+ getFileLines: (filePath) => {
481
+ const cached = this.fileLinesCache.get(filePath);
482
+ if (cached !== undefined)
483
+ return cached;
484
+ const source = this.readFileCached(filePath);
485
+ const lines = source === null ? null : source.split(/\r?\n/);
486
+ this.fileLinesCache.set(filePath, lines);
487
+ return lines;
401
488
  },
402
489
  getProjectRoot: () => this.projectRoot,
403
490
  getAllFiles: () => {
@@ -500,7 +587,7 @@ class ReferenceResolver {
500
587
  // `.ts` index barrel and silently break the chain (#629). Re-key
501
588
  // the parse on the barrel's extension so the chase works no matter
502
589
  // what kind of file imports through it.
503
- const isJsFamily = /\.(?:d\.ts|[cm]?tsx?|[cm]?jsx?)$/i.test(filePath);
590
+ const isJsFamily = /\.(?:d\.ts|[cm]?tsx?|[cm]?jsx?|ets)$/i.test(filePath);
504
591
  const reExports = (0, import_resolver_1.extractReExports)(content, isJsFamily ? 'typescript' : language);
505
592
  this.reExportCache.set(filePath, reExports);
506
593
  return reExports;
@@ -667,13 +754,36 @@ class ReferenceResolver {
667
754
  if (this.isBuiltInOrExternal(ref)) {
668
755
  return null;
669
756
  }
757
+ // CFML component paths in inheritance (#1152): `extends="coldbox.system.web.
758
+ // Controller"` names the supertype by its dot-separated path (or `extends=
759
+ // "../base"` by relative file path) — the graph indexes the class under its
760
+ // final segment only, so these die at the fast pre-filter below and never
761
+ // resolved. Handled by a dedicated path-corroborated matcher, gated to
762
+ // inheritance refs only (a dotted `calls` ref is a member-access chain, not
763
+ // a component path). No fallthrough on miss: the full path string can only
764
+ // ever mis-match downstream, and an unresolvable supertype usually lives in
765
+ // an out-of-repo library (mxunit, testbox) — silent beats wrong.
766
+ if ((ref.language === 'cfml' || ref.language === 'cfscript') &&
767
+ (ref.referenceKind === 'extends' || ref.referenceKind === 'implements') &&
768
+ (ref.referenceName.includes('.') || ref.referenceName.includes('/'))) {
769
+ return this.resolveCfmlComponentPath(ref);
770
+ }
670
771
  // Fast pre-filter: skip if no symbol with this name exists anywhere
671
772
  // AND the name doesn't match a local import. The import escape is
672
773
  // necessary because re-export rename chains (`import { login }
673
774
  // from './barrel'` where the barrel has `export { signIn as login }
674
775
  // from './auth'`) intentionally call a name that has no
675
776
  // declaration anywhere — only the renamed upstream symbol does.
676
- if (!this.hasAnyPossibleMatch(ref.referenceName) &&
777
+ // ArkTS chained-attribute refs carry a leading dot (`.titleStyle`) that
778
+ // routes them to the decorator-gated matcher; the symbol itself is
779
+ // indexed under the bare name, so the existence check strips the dot.
780
+ // Nix static path imports (`import ./x.nix`) name a FILE, not a symbol —
781
+ // they bypass the symbol-existence check and resolve via resolveViaImport.
782
+ const existenceName = ref.language === 'arkts' && ref.referenceName.startsWith('.')
783
+ ? ref.referenceName.slice(1)
784
+ : ref.referenceName;
785
+ if (!(0, import_resolver_1.isNixPathImportRef)(ref) &&
786
+ !this.hasAnyPossibleMatch(existenceName) &&
677
787
  !this.matchesAnyImport(ref) &&
678
788
  !this.frameworks.some((f) => f.claimsReference?.(ref.referenceName))) {
679
789
  return null;
@@ -739,13 +849,40 @@ class ReferenceResolver {
739
849
  // If that didn't find the file, do NOT fall back to the symbol
740
850
  // name-matcher — it would mis-connect e.g. "inc/db.php" to an unrelated
741
851
  // db.php elsewhere in the tree (a wrong edge is worse than none, #660).
742
- if ((0, import_resolver_1.isPhpIncludePathRef)(ref)) {
852
+ // Terraform refs are directory-scoped by language semantics — the
853
+ // framework resolver IS the whole rulebook (`var.X` can never legally
854
+ // bind outside its module directory), so the name-matcher's
855
+ // qualified-name fallback would only ever add wrong cross-module edges.
856
+ // Nix static path imports are file references for the same reason —
857
+ // falling through would let "./x.nix" name-match an unrelated node.
858
+ if ((0, import_resolver_1.isPhpIncludePathRef)(ref) || (0, import_resolver_1.isCobolCopybookRef)(ref) || (0, import_resolver_1.isNixPathImportRef)(ref) || ref.language === 'terraform') {
743
859
  return candidates.length > 0
744
860
  ? candidates.reduce((best, curr) => curr.confidence > best.confidence ? curr : best)
745
861
  : null;
746
862
  }
747
863
  // Strategy 3: Try name matching
748
- const nameResult = this.gateLanguage((0, name_matcher_1.matchReference)(ref, this.context), ref);
864
+ let nameResult = this.gateLanguage((0, name_matcher_1.matchReference)(ref, this.context), ref);
865
+ // Nix has no ambient cross-file namespace — a callee binds lexically
866
+ // (same file) or through explicit import/callPackage wiring (the import
867
+ // path above). A cross-file name match is wrong by construction: every
868
+ // module `inherit (lib) mkOption`s the same nixpkgs helpers, so the
869
+ // matcher would link each `mkOption` call to whichever file's inherit
870
+ // binding it happened to pick. Same-file matches only.
871
+ if (nameResult) {
872
+ const target = this.queries.getNodeById(nameResult.targetNodeId);
873
+ if (ref.language === 'nix') {
874
+ if (!target || target.filePath !== ref.filePath) {
875
+ nameResult = null;
876
+ }
877
+ }
878
+ else if (target && target.language === 'nix') {
879
+ // The reverse direction is just as impossible: no other language can
880
+ // symbolically call into a .nix binding (interop is eval/CLI, never a
881
+ // linkable symbol) — without this, a Python script's `split()` lands
882
+ // on some module's `split = ...` binding as a low-confidence match.
883
+ nameResult = null;
884
+ }
885
+ }
749
886
  if (nameResult) {
750
887
  candidates.push(nameResult);
751
888
  }
@@ -758,6 +895,15 @@ class ReferenceResolver {
758
895
  CHAIN_SHAPE.test(ref.referenceName)) {
759
896
  this.deferredChainRefs.push(ref);
760
897
  }
898
+ else if (
899
+ // PHP `$this->prop->method()` (encoded `this->prop.method`): its method
900
+ // may live on the property's declared supertype, resolvable only once
901
+ // implements/extends edges exist — defer to the same conformance pass.
902
+ ref.referenceKind === 'calls' &&
903
+ ref.language === 'php' &&
904
+ PHP_PROP_SHAPE.test(ref.referenceName)) {
905
+ this.deferredChainRefs.push(ref);
906
+ }
761
907
  return null;
762
908
  }
763
909
  // Return highest confidence candidate
@@ -804,6 +950,18 @@ class ReferenceResolver {
804
950
  metadata: {
805
951
  confidence: ref.confidence,
806
952
  resolvedBy: ref.resolvedBy,
953
+ // The ORIGINAL reference text (and kind, when edge-kind promotion
954
+ // rewrote it — calls→instantiates, extends→implements,
955
+ // function_ref→references). If this edge's target is later removed
956
+ // by a re-index, the edge is resurrected as exactly this ref and
957
+ // re-resolved (#1240 removal case) — a faithful resurrection, so
958
+ // re-resolution can never bind anywhere a full re-index wouldn't.
959
+ // Reconstruction from the target node's name instead would strip
960
+ // receiver/qualifier context (`h.greet` → `greet`) and risk a
961
+ // wrong rebind; edges without refName (pre-#1240, synthesized) are
962
+ // deliberately NOT resurrected for the same reason.
963
+ refName: ref.original.referenceName,
964
+ ...(ref.original.referenceKind !== kind ? { refKind: ref.original.referenceKind } : {}),
807
965
  // Uniform marker for function-as-value edges (#756), regardless of
808
966
  // which strategy resolved them (import vs matchFunctionRef) — lets
809
967
  // tooling label "callback registration" and lets validation diff
@@ -832,6 +990,60 @@ class ReferenceResolver {
832
990
  referenceKind: r.original.referenceKind,
833
991
  })));
834
992
  }
993
+ // Park unresolvable refs as status='failed' — parity with
994
+ // resolveAndPersistBatched. Deleting them was wrong (#1240): a ref whose
995
+ // own file never changes is otherwise gone forever, so when a DIFFERENT
996
+ // file later gains the export/symbol that would satisfy it, no sync can
997
+ // recreate the edge — only a full re-index. Failed rows are excluded from
998
+ // the pending readers, which preserves the #1187 orphan sweep's
999
+ // invariant in status form: after a COMPLETED pass nothing it processed
1000
+ // is still 'pending', so any pending row at rest belongs to an
1001
+ // interrupted run and the sweep can key off the pending count.
1002
+ if (result.unresolved.length > 0) {
1003
+ this.queries.markReferencesFailed(result.unresolved.map((r) => ({
1004
+ fromNodeId: r.fromNodeId,
1005
+ referenceName: r.referenceName,
1006
+ referenceKind: r.referenceKind,
1007
+ })));
1008
+ }
1009
+ return result;
1010
+ }
1011
+ /**
1012
+ * Yielding counterpart of {@link resolveAndPersist} for a caller-supplied
1013
+ * ref list — used by sync's failed-ref retry pass (#1240). Same persistence
1014
+ * semantics: resolved refs become edges and their rows are deleted;
1015
+ * still-unresolvable refs are (re-)marked failed (a no-op for rows already
1016
+ * in that status). Yields per-ref because sync can run on the daemon's
1017
+ * liveness-watchdog thread (#850/#1091) and a retry set is unbounded when
1018
+ * a large edit lands many popular symbol names at once.
1019
+ */
1020
+ async resolveAndPersistListYielding(refs) {
1021
+ const maybeYield = (0, cooperative_yield_1.createYielder)();
1022
+ const result = await this.resolveBatchYielding(refs, maybeYield);
1023
+ const PERSIST_CHUNK = 1000;
1024
+ const edges = this.createEdges(result.resolved);
1025
+ for (let i = 0; i < edges.length; i += PERSIST_CHUNK) {
1026
+ this.queries.insertEdges(edges.slice(i, i + PERSIST_CHUNK));
1027
+ await maybeYield();
1028
+ }
1029
+ const resolvedKeys = result.resolved.map((r) => ({
1030
+ fromNodeId: r.original.fromNodeId,
1031
+ referenceName: r.original.referenceName,
1032
+ referenceKind: r.original.referenceKind,
1033
+ }));
1034
+ for (let i = 0; i < resolvedKeys.length; i += PERSIST_CHUNK) {
1035
+ this.queries.deleteSpecificResolvedReferences(resolvedKeys.slice(i, i + PERSIST_CHUNK));
1036
+ await maybeYield();
1037
+ }
1038
+ const unresolvedKeys = result.unresolved.map((r) => ({
1039
+ fromNodeId: r.fromNodeId,
1040
+ referenceName: r.referenceName,
1041
+ referenceKind: r.referenceKind,
1042
+ }));
1043
+ for (let i = 0; i < unresolvedKeys.length; i += PERSIST_CHUNK) {
1044
+ this.queries.markReferencesFailed(unresolvedKeys.slice(i, i + PERSIST_CHUNK));
1045
+ await maybeYield();
1046
+ }
835
1047
  return result;
836
1048
  }
837
1049
  /**
@@ -862,11 +1074,15 @@ class ReferenceResolver {
862
1074
  const maybeYield = (0, cooperative_yield_1.createYielder)();
863
1075
  const resolved = [];
864
1076
  for (const ref of deferred) {
865
- // `::`-receiver languages (Rust) split on `::` (matchScopedCallChain);
1077
+ // PHP `this->prop.method` resolves via matchMethodCall (declared-type
1078
+ // inference + resolveMethodOnType conformance walk); `::`-receiver
1079
+ // languages (Rust) split on `::` (matchScopedCallChain); other
866
1080
  // dotted-receiver languages on `.` (matchDottedCallChain).
867
- const chainMatch = SCOPED_CHAIN_LANGUAGES.has(ref.language)
868
- ? (0, name_matcher_1.matchScopedCallChain)(ref, this.context)
869
- : (0, name_matcher_1.matchDottedCallChain)(ref, this.context);
1081
+ const chainMatch = (ref.language === 'php' && PHP_PROP_SHAPE.test(ref.referenceName))
1082
+ ? (0, name_matcher_1.matchMethodCall)(ref, this.context)
1083
+ : SCOPED_CHAIN_LANGUAGES.has(ref.language)
1084
+ ? (0, name_matcher_1.matchScopedCallChain)(ref, this.context)
1085
+ : (0, name_matcher_1.matchDottedCallChain)(ref, this.context);
870
1086
  const match = this.gateLanguage(chainMatch, ref);
871
1087
  if (match)
872
1088
  resolved.push(match);
@@ -882,45 +1098,60 @@ class ReferenceResolver {
882
1098
  return edges.length;
883
1099
  }
884
1100
  /**
885
- * Resolve one batch in smaller sub-chunks, yielding to the event loop between
886
- * them so the #850 liveness heartbeat can fire on a slow/dense batch (#1091).
887
- * Behaviourally identical to a single `resolveAll(batch)`: `warmCaches()` is
888
- * idempotent (guarded) and `resolveOne` is independent per ref, so splitting
889
- * and re-merging changes only timing, never which edges get created. Falls
890
- * through to a plain `resolveAll` when the batch is already small.
1101
+ * Resolve one batch with a yield checkpoint between EVERY ref so the #850
1102
+ * liveness heartbeat can fire on a slow/dense batch (#1091). The checkpoint
1103
+ * granularity is per-ref not per-N-refs because per-ref cost is unbounded
1104
+ * in the worst case (a collision-heavy method name whose candidate set misses
1105
+ * the LRU re-fetches tens of thousands of rows): any fixed N multiplies that
1106
+ * worst case into the watchdog window, which is how v1.2.0 still got killed
1107
+ * at "Resolving refs" on large Java monorepos (#1122). `maybeYield()` is a
1108
+ * ~ns time check when under budget, so per-ref checkpoints cost nothing.
1109
+ * Behaviourally identical to `resolveAll(batch)`: `warmCaches()` is
1110
+ * idempotent (guarded) and `resolveOne` is independent per ref, so yielding
1111
+ * between refs changes only timing, never which edges get created.
891
1112
  */
892
- async resolveBatchYielding(batch, maybeYield, subChunkSize = 500) {
893
- if (batch.length <= subChunkSize)
894
- return this.resolveAll(batch);
1113
+ async resolveBatchYielding(batch, maybeYield) {
1114
+ this.warmCaches();
895
1115
  const resolved = [];
896
1116
  const unresolved = [];
897
1117
  const byMethod = {};
898
- let total = 0;
899
- let resolvedCount = 0;
900
- let unresolvedCount = 0;
901
- for (let i = 0; i < batch.length; i += subChunkSize) {
902
- const chunk = this.resolveAll(batch.slice(i, i + subChunkSize));
903
- for (const r of chunk.resolved)
904
- resolved.push(r);
905
- for (const u of chunk.unresolved)
906
- unresolved.push(u);
907
- total += chunk.stats.total;
908
- resolvedCount += chunk.stats.resolved;
909
- unresolvedCount += chunk.stats.unresolved;
910
- for (const [m, c] of Object.entries(chunk.stats.byMethod)) {
911
- byMethod[m] = (byMethod[m] || 0) + c;
1118
+ for (const raw of batch) {
1119
+ const ref = {
1120
+ fromNodeId: raw.fromNodeId,
1121
+ referenceName: raw.referenceName,
1122
+ referenceKind: raw.referenceKind,
1123
+ line: raw.line,
1124
+ column: raw.column,
1125
+ filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId),
1126
+ language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId),
1127
+ };
1128
+ const result = this.resolveOne(ref);
1129
+ if (result) {
1130
+ resolved.push(result);
1131
+ byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
1132
+ }
1133
+ else {
1134
+ unresolved.push(ref);
912
1135
  }
913
1136
  await maybeYield();
914
1137
  }
915
- return { resolved, unresolved, stats: { total, resolved: resolvedCount, unresolved: unresolvedCount, byMethod } };
1138
+ return {
1139
+ resolved,
1140
+ unresolved,
1141
+ stats: {
1142
+ total: batch.length,
1143
+ resolved: resolved.length,
1144
+ unresolved: unresolved.length,
1145
+ byMethod,
1146
+ },
1147
+ };
916
1148
  }
917
1149
  /**
918
1150
  * Resolve and persist in batches to keep memory bounded.
919
1151
  * Processes unresolved references in chunks, persisting edges and cleaning
920
1152
  * up resolved refs after each batch to avoid accumulating large arrays.
921
1153
  */
922
- async resolveAndPersistBatched(onProgress, batchSize = 5000) {
923
- this.warmCaches();
1154
+ async resolveAndPersistBatched(onProgress, batchSize = 5000, onSynthesisProgress) {
924
1155
  // Resolution runs on the indexer's MAIN thread, and the #850 liveness
925
1156
  // watchdog SIGKILLs a process whose event loop stalls past its window (60s
926
1157
  // by default). A single dense batch's resolveAll — or the synthesis pass
@@ -928,6 +1159,7 @@ class ReferenceResolver {
928
1159
  // (#1091). A shared yielder lets both give the watchdog heartbeat a regular
929
1160
  // window to fire; see ./cooperative-yield.
930
1161
  const maybeYield = (0, cooperative_yield_1.createYielder)();
1162
+ await this.warmCachesYielding(maybeYield);
931
1163
  const total = this.queries.getUnresolvedReferencesCount();
932
1164
  let processed = 0;
933
1165
  const aggregateStats = {
@@ -936,34 +1168,52 @@ class ReferenceResolver {
936
1168
  unresolved: 0,
937
1169
  byMethod: {},
938
1170
  };
939
- // Process in batches. We always read from offset 0 because resolved refs
940
- // are deleted after each batch, shifting the remaining rows forward.
1171
+ // Process in batches. We always read from offset 0 because every ref the
1172
+ // batch processed leaves the pending set (resolved rows are deleted,
1173
+ // unresolvable ones flip to status='failed'), shifting the remaining
1174
+ // pending rows forward.
941
1175
  let prevRemaining = Number.POSITIVE_INFINITY;
942
1176
  while (true) {
943
1177
  const batch = this.queries.getUnresolvedReferencesBatch(0, batchSize);
944
1178
  if (batch.length === 0)
945
1179
  break;
946
1180
  const result = await this.resolveBatchYielding(batch, maybeYield);
1181
+ // Persist in bounded sub-transactions with yields between: a whole
1182
+ // batch's edge insert / keyed deletes are otherwise one solid
1183
+ // synchronous span each on a multi-GB index, sitting BETWEEN the
1184
+ // per-ref yields — the last unyielded stretch of the resolution loop.
1185
+ // Crash semantics are unchanged (already several transactions): edges
1186
+ // land before their refs are deleted, so a kill mid-way re-resolves
1187
+ // the remainder idempotently on the next run/sweep (#1187).
1188
+ const PERSIST_CHUNK = 1000;
947
1189
  // Persist edges immediately
948
1190
  const edges = this.createEdges(result.resolved);
949
- if (edges.length > 0) {
950
- this.queries.insertEdges(edges);
1191
+ for (let i = 0; i < edges.length; i += PERSIST_CHUNK) {
1192
+ this.queries.insertEdges(edges.slice(i, i + PERSIST_CHUNK));
1193
+ await maybeYield();
951
1194
  }
952
1195
  // Clean up resolved refs so they don't appear in the next batch
953
- if (result.resolved.length > 0) {
954
- this.queries.deleteSpecificResolvedReferences(result.resolved.map((r) => ({
955
- fromNodeId: r.original.fromNodeId,
956
- referenceName: r.original.referenceName,
957
- referenceKind: r.original.referenceKind,
958
- })));
1196
+ const resolvedKeys = result.resolved.map((r) => ({
1197
+ fromNodeId: r.original.fromNodeId,
1198
+ referenceName: r.original.referenceName,
1199
+ referenceKind: r.original.referenceKind,
1200
+ }));
1201
+ for (let i = 0; i < resolvedKeys.length; i += PERSIST_CHUNK) {
1202
+ this.queries.deleteSpecificResolvedReferences(resolvedKeys.slice(i, i + PERSIST_CHUNK));
1203
+ await maybeYield();
959
1204
  }
960
- // Delete unresolvable refs from this batch to avoid re-processing them
961
- if (result.unresolved.length > 0) {
962
- this.queries.deleteSpecificResolvedReferences(result.unresolved.map((r) => ({
963
- fromNodeId: r.fromNodeId,
964
- referenceName: r.referenceName,
965
- referenceKind: r.referenceKind,
966
- })));
1205
+ // Park unresolvable refs from this batch as status='failed' so they
1206
+ // leave the pending set (the batch reader and non-progress guard below
1207
+ // only see pending rows) but stay retryable when a later sync adds a
1208
+ // symbol that could satisfy them (#1240).
1209
+ const unresolvedKeys = result.unresolved.map((r) => ({
1210
+ fromNodeId: r.fromNodeId,
1211
+ referenceName: r.referenceName,
1212
+ referenceKind: r.referenceKind,
1213
+ }));
1214
+ for (let i = 0; i < unresolvedKeys.length; i += PERSIST_CHUNK) {
1215
+ this.queries.markReferencesFailed(unresolvedKeys.slice(i, i + PERSIST_CHUNK));
1216
+ await maybeYield();
967
1217
  }
968
1218
  // Aggregate stats
969
1219
  aggregateStats.total += result.stats.total;
@@ -976,19 +1226,22 @@ class ReferenceResolver {
976
1226
  onProgress?.(processed, total);
977
1227
  // Yield so progress UI can render between batches
978
1228
  await new Promise(resolve => setImmediate(resolve));
979
- // If nothing was resolved or removed in this batch, we'd loop forever
980
- // on the same rows. Break to avoid infinite loop.
981
- if (result.resolved.length === 0 && result.unresolved.length === batch.length) {
982
- break;
983
- }
1229
+ // NOTE: there used to be an extra early break here when a batch resolved
1230
+ // nothing (`result.unresolved.length === batch.length`). That was wrong:
1231
+ // an all-unresolvable batch still DELETES its rows (progress), yet the
1232
+ // break abandoned every batch after it in the same run — on a repo whose
1233
+ // first 5000 refs are all external/stdlib calls, resolution stopped at
1234
+ // batch one and left the rest of the table as permanent orphans (#1187).
1235
+ // The count-based guard below catches the true no-progress case.
984
1236
  // Non-progress guard (defense-in-depth). Because we re-read from offset 0
985
- // each pass, the unresolved_refs table MUST shrink every iteration — both
986
- // resolved and unresolved refs are deleted above. If it didn't shrink, a
1237
+ // each pass, the PENDING population MUST shrink every iteration — resolved
1238
+ // refs are deleted and unresolvable ones are marked failed above, and both
1239
+ // leave the pending set the batch reader sees. If it didn't shrink, a
987
1240
  // resolver returned a match whose `original.referenceName` differs from the
988
- // stored row, so the keyed delete no-ops, and we'd re-read + re-resolve +
989
- // re-insert the same rows forever (the runaway that grew a 99-file repo to
990
- // 5M edges / 1.4 GB before the Go-fallback fix). Stop rather than grow the
991
- // graph without bound.
1241
+ // stored row, so the keyed delete/update no-ops, and we'd re-read +
1242
+ // re-resolve + re-insert the same rows forever (the runaway that grew a
1243
+ // 99-file repo to 5M edges / 1.4 GB before the Go-fallback fix). Stop
1244
+ // rather than grow the graph without bound.
992
1245
  const remaining = this.queries.getUnresolvedReferencesCount();
993
1246
  if (remaining >= prevRemaining)
994
1247
  break;
@@ -999,7 +1252,7 @@ class ReferenceResolver {
999
1252
  // callbacks) that static parsing leaves out. Best-effort — never fail the
1000
1253
  // index on it. See docs/design/callback-edge-synthesis.md.
1001
1254
  try {
1002
- aggregateStats.byMethod['callback-synthesis'] = await (0, callback_synthesizer_1.synthesizeCallbackEdges)(this.queries, this.context);
1255
+ aggregateStats.byMethod['callback-synthesis'] = await (0, callback_synthesizer_1.synthesizeCallbackEdges)(this.queries, this.context, onSynthesisProgress);
1003
1256
  }
1004
1257
  catch {
1005
1258
  // synthesis is additive and optional; ignore failures
@@ -1022,11 +1275,18 @@ class ReferenceResolver {
1022
1275
  isBuiltInOrExternal(ref) {
1023
1276
  const name = ref.referenceName;
1024
1277
  const isJsTs = ref.language === 'typescript' || ref.language === 'javascript'
1025
- || ref.language === 'tsx' || ref.language === 'jsx';
1278
+ || ref.language === 'tsx' || ref.language === 'jsx' || ref.language === 'arkts';
1026
1279
  // JavaScript/TypeScript built-ins
1027
1280
  if (isJsTs && JS_BUILT_INS.has(name)) {
1028
1281
  return true;
1029
1282
  }
1283
+ // ArkTS resource-reference intrinsics — `$r('app.string.x')` /
1284
+ // `$rawfile('x.png')` are framework-provided and appear dozens of times
1285
+ // per UI file; without this they can resolve to a stray same-named
1286
+ // symbol (e.g. a checked-in hvigor wrapper's `$r`).
1287
+ if (ref.language === 'arkts' && (name === '$r' || name === '$rawfile')) {
1288
+ return true;
1289
+ }
1030
1290
  // Common JS/TS library calls (console.log, Math.floor, JSON.parse)
1031
1291
  if (isJsTs && (name.startsWith('console.') || name.startsWith('Math.') || name.startsWith('JSON.'))) {
1032
1292
  return true;
@@ -1191,6 +1451,96 @@ class ReferenceResolver {
1191
1451
  const target = found.values().next().value;
1192
1452
  return { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'import' };
1193
1453
  }
1454
+ /**
1455
+ * Resolve a CFML inheritance reference written as a component path (#1152).
1456
+ * Two forms exist in real code:
1457
+ *
1458
+ * - Dotted: `extends="coldbox.system.web.Controller"` — dots are directory
1459
+ * separators from the webroot or a CFML mapping. Mappings live in server
1460
+ * config / Application.cfc, so the leading segments may not exist in the
1461
+ * repo at all (in the coldbox repo itself the path is `system/web/
1462
+ * Controller.cfc` — the `coldbox.` root IS the repo). Matched by final
1463
+ * segment (the class), corroborated right-to-left against the candidate's
1464
+ * parent directories.
1465
+ * - Relative: `extends="../base"` / `extends="./base"` (the FW/1 style) —
1466
+ * resolved against the referencing file's own directory.
1467
+ *
1468
+ * Conservative by design: a candidate needs at least one corroborating
1469
+ * directory segment (a dotted path whose only same-named class sits in an
1470
+ * unrelated directory is almost always an out-of-repo library supertype —
1471
+ * mxunit/testbox/coldbox-as-dependency), and a corroboration tie yields no
1472
+ * edge. Directory comparison is case-insensitive (CFML path resolution is);
1473
+ * the class segment itself is matched exactly, which real code satisfies —
1474
+ * dotted paths are written to match the on-disk file name.
1475
+ */
1476
+ resolveCfmlComponentPath(ref) {
1477
+ const cfmlCandidates = (name) => this.context
1478
+ .getNodesByName(name)
1479
+ .filter((n) => (n.kind === 'class' || n.kind === 'interface') &&
1480
+ (n.language === 'cfml' || n.language === 'cfscript'));
1481
+ const norm = (p) => p.replace(/\\/g, '/').toLowerCase();
1482
+ // Relative-path form: `../base`, `./base`, `sub/thing` — resolve against
1483
+ // the referencing file's directory and require an exact (case-insensitive)
1484
+ // file match.
1485
+ if (ref.referenceName.includes('/')) {
1486
+ const rel = ref.referenceName.replace(/\.cfc$/i, '');
1487
+ const fromDir = ref.filePath.replace(/\\/g, '/').split('/').slice(0, -1);
1488
+ const parts = [...fromDir];
1489
+ for (const seg of rel.split('/')) {
1490
+ if (seg === '' || seg === '.')
1491
+ continue;
1492
+ if (seg === '..') {
1493
+ if (parts.length === 0)
1494
+ return null; // escapes the project root
1495
+ parts.pop();
1496
+ }
1497
+ else {
1498
+ parts.push(seg);
1499
+ }
1500
+ }
1501
+ const wantPath = norm(parts.join('/') + '.cfc');
1502
+ const className = parts[parts.length - 1];
1503
+ if (!className)
1504
+ return null;
1505
+ const target = cfmlCandidates(className).find((c) => norm(c.filePath) === wantPath);
1506
+ return target
1507
+ ? { original: ref, targetNodeId: target.id, confidence: 0.95, resolvedBy: 'file-path' }
1508
+ : null;
1509
+ }
1510
+ // Dotted form.
1511
+ const segments = ref.referenceName.split('.').map((s) => s.trim()).filter(Boolean);
1512
+ if (segments.length < 2)
1513
+ return null;
1514
+ const className = segments[segments.length - 1];
1515
+ const dirSegments = segments.slice(0, -1);
1516
+ let best = null;
1517
+ let bestScore = 0;
1518
+ let tie = false;
1519
+ for (const cand of cfmlCandidates(className)) {
1520
+ const dirs = cand.filePath.replace(/\\/g, '/').split('/').slice(0, -1);
1521
+ // Count matching directory segments right-to-left: for
1522
+ // `coldbox.system.web.Controller` vs `system/web/Controller.cfc`,
1523
+ // `web` and `system` match, then the repo root ends the run → score 2.
1524
+ let score = 0;
1525
+ while (score < dirSegments.length &&
1526
+ score < dirs.length &&
1527
+ dirSegments[dirSegments.length - 1 - score].toLowerCase() ===
1528
+ dirs[dirs.length - 1 - score].toLowerCase()) {
1529
+ score++;
1530
+ }
1531
+ if (score > bestScore) {
1532
+ best = cand;
1533
+ bestScore = score;
1534
+ tie = false;
1535
+ }
1536
+ else if (score === bestScore && score > 0) {
1537
+ tie = true;
1538
+ }
1539
+ }
1540
+ if (!best || bestScore === 0 || tie)
1541
+ return null;
1542
+ return { original: ref, targetNodeId: best.id, confidence: 0.9, resolvedBy: 'qualified-name' };
1543
+ }
1194
1544
  /**
1195
1545
  * Resolve a `this.<member>` function-as-value reference (#756/#808) to the
1196
1546
  * ENCLOSING CLASS's own member — never a same-named symbol elsewhere. The