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
@@ -46,6 +46,7 @@ const query_utils_1 = require("../search/query-utils");
46
46
  const query_parser_1 = require("../search/query-parser");
47
47
  const generated_detection_1 = require("../extraction/generated-detection");
48
48
  const arkts_1 = require("../extraction/languages/arkts");
49
+ const identifier_segments_1 = require("../search/identifier-segments");
49
50
  /**
50
51
  * Path-only heuristic for files that should not be candidates for
51
52
  * "dominant file" detection: test/spec files and tool-generated files.
@@ -69,6 +70,14 @@ function isLowValueFile(filePath) {
69
70
  (0, generated_detection_1.isGeneratedFile)(filePath));
70
71
  }
71
72
  const SQLITE_PARAM_CHUNK_SIZE = 500;
73
+ /**
74
+ * Last segment of a (possibly dotted/qualified) reference name — the part a
75
+ * new symbol's plain node name could match: 'util.greet' → 'greet'.
76
+ */
77
+ function referenceNameTail(referenceName) {
78
+ const idx = Math.max(referenceName.lastIndexOf('.'), referenceName.lastIndexOf(':'));
79
+ return idx >= 0 ? referenceName.slice(idx + 1) : referenceName;
80
+ }
72
81
  /**
73
82
  * Convert database row to Node object
74
83
  */
@@ -136,6 +145,8 @@ class QueryBuilder {
136
145
  // whole project, not a symbol, so it carries no discriminative signal (#720).
137
146
  // Set once by the HomeGraph instance; empty by default (no down-weighting).
138
147
  projectNameTokens = new Set();
148
+ segmentedNames = new Set();
149
+ static MAX_SEGMENTED_NAMES = 65536;
139
150
  // Node cache for frequently accessed nodes (LRU-style, max 1000 entries)
140
151
  nodeCache = new Map();
141
152
  maxCacheSize = 1000;
@@ -254,6 +265,10 @@ class QueryBuilder {
254
265
  returnType: node.returnType ?? null,
255
266
  updatedAt: node.updatedAt ?? Date.now(),
256
267
  });
268
+ // Segment vocabulary rides the same write path so it never drifts ahead of
269
+ // the nodes it describes. File/import nodes are excluded (#1144).
270
+ if (this.isSegmentableKind(node.kind))
271
+ this.insertNameSegments(node.name);
257
272
  }
258
273
  /**
259
274
  * Insert multiple nodes in a transaction
@@ -325,6 +340,9 @@ class QueryBuilder {
325
340
  returnType: node.returnType ?? null,
326
341
  updatedAt: node.updatedAt ?? Date.now(),
327
342
  });
343
+ // Framework post-extract renames must reach the segment vocabulary (#1141).
344
+ if (this.isSegmentableKind(node.kind))
345
+ this.insertNameSegments(node.name);
328
346
  }
329
347
  /**
330
348
  * Delete a node by ID
@@ -352,6 +370,104 @@ class QueryBuilder {
352
370
  }
353
371
  this.stmts.deleteNodesByFile.run(filePath);
354
372
  }
373
+ /** Which node kinds contribute their name to the segment vocabulary. */
374
+ isSegmentableKind(kind) {
375
+ return kind !== 'file' && kind !== 'import';
376
+ }
377
+ /** Write `name`'s segments into name_segment_vocab (idempotent). */
378
+ insertNameSegments(name) {
379
+ if (this.segmentedNames.has(name))
380
+ return;
381
+ if (this.segmentedNames.size >= QueryBuilder.MAX_SEGMENTED_NAMES)
382
+ this.segmentedNames.clear();
383
+ this.segmentedNames.add(name);
384
+ if (!this.stmts.insertNameSegment) {
385
+ this.stmts.insertNameSegment = this.db.prepare('INSERT OR IGNORE INTO name_segment_vocab (segment, name) VALUES (?, ?)');
386
+ }
387
+ for (const segment of (0, identifier_segments_1.splitIdentifierSegments)(name)) {
388
+ this.stmts.insertNameSegment.run(segment, name);
389
+ }
390
+ }
391
+ // ===========================================================================
392
+ // Name-segment vocabulary (prompt-hook graph-derived gate)
393
+ // ===========================================================================
394
+ /** Wipe the segment vocabulary. A full index calls this at its start; the
395
+ * node write path repopulates it as files (re-)index, so the end state is
396
+ * exactly the current names with no orphan rows. */
397
+ clearNameSegmentVocab() {
398
+ this.db.exec('DELETE FROM name_segment_vocab');
399
+ this.segmentedNames.clear();
400
+ }
401
+ /** True when the vocab has no rows — an index built before the table existed.
402
+ * `sync` uses this to heal such databases (see rebuildNameSegmentVocabFrom). */
403
+ isNameSegmentVocabEmpty() {
404
+ const row = this.db.prepare('SELECT 1 FROM name_segment_vocab LIMIT 1').get();
405
+ return row === undefined;
406
+ }
407
+ /** One page of distinct segmentable node names, for batched vocab rebuilds
408
+ * (file basenames and import specifiers are excluded from the vocab — see
409
+ * insertNode). */
410
+ getDistinctNodeNames(limit, offset) {
411
+ const rows = this.db
412
+ .prepare("SELECT DISTINCT name FROM nodes WHERE kind NOT IN ('file', 'import') ORDER BY name LIMIT ? OFFSET ?")
413
+ .all(limit, offset);
414
+ return rows.map((r) => r.name);
415
+ }
416
+ /** Insert segments for a batch of names in one transaction (vocab heal path). */
417
+ insertNameSegmentsBatch(names) {
418
+ this.db.transaction(() => {
419
+ for (const name of names)
420
+ this.insertNameSegments(name);
421
+ })();
422
+ }
423
+ /**
424
+ * Names whose segments cover at least `minWords` distinct PROMPT WORDS —
425
+ * the co-occurrence probe behind the prompt hook's medium tier: the words
426
+ * "state" and "machine" both being segments of `OrderStateMachine` is strong
427
+ * evidence the prompt names that symbol in prose. Ordered by coverage.
428
+ *
429
+ * Takes (segment variant → original word) pairs and folds variants back to
430
+ * their word INSIDE the SQL: a name matching both `service` and `services`
431
+ * counts ONE word, not two. Counting raw variants let plural-variant pairs
432
+ * of a single word tie with genuine two-word matches and — because ORDER
433
+ * BY/LIMIT run here, before any JS-side re-check — crowd a real match past
434
+ * the LIMIT on vocab-heavy repos (#1146).
435
+ */
436
+ getSegmentCoOccurrence(variants, minWords, limit) {
437
+ if (variants.length === 0)
438
+ return [];
439
+ const placeholders = variants.map(() => '?').join(', ');
440
+ const whens = variants.map(() => 'WHEN ? THEN ?').join(' ');
441
+ const rows = this.db
442
+ .prepare(`SELECT name, COUNT(DISTINCT CASE segment ${whens} END) AS matches
443
+ FROM name_segment_vocab
444
+ WHERE segment IN (${placeholders})
445
+ GROUP BY name
446
+ HAVING matches >= ?
447
+ ORDER BY matches DESC, length(name) ASC
448
+ LIMIT ?`)
449
+ .all(...variants.flatMap((v) => [v.segment, v.word]), ...variants.map((v) => v.segment), minWords, limit);
450
+ return rows;
451
+ }
452
+ /** How many distinct names each segment appears in — the rarity signal that
453
+ * separates a discriminative word ("checkout") from a ubiquitous one ("state"). */
454
+ getSegmentNameCounts(segments) {
455
+ if (segments.length === 0)
456
+ return new Map();
457
+ const placeholders = segments.map(() => '?').join(', ');
458
+ const rows = this.db
459
+ .prepare(`SELECT segment, COUNT(*) AS n FROM name_segment_vocab
460
+ WHERE segment IN (${placeholders}) GROUP BY segment`)
461
+ .all(...segments);
462
+ return new Map(rows.map((r) => [r.segment, r.n]));
463
+ }
464
+ /** Names containing the given segment (rare-single-word tier). */
465
+ getNamesForSegment(segment, limit) {
466
+ const rows = this.db
467
+ .prepare('SELECT name FROM name_segment_vocab WHERE segment = ? ORDER BY length(name) ASC LIMIT ?')
468
+ .all(segment, limit);
469
+ return rows.map((r) => r.name);
470
+ }
355
471
  /**
356
472
  * Get a node by ID
357
473
  */
@@ -645,6 +761,33 @@ class QueryBuilder {
645
761
  yield rowToNode(row);
646
762
  }
647
763
  }
764
+ /**
765
+ * Stream nodes of a language whose `decorators` JSON array contains a
766
+ * quoted decorator name. LIKE is a substring filter (a decorator name can
767
+ * appear as a substring of another), so callers must still exact-check
768
+ * `node.decorators.includes(decorator)`. Exists so the kotlin expect/actual
769
+ * synthesizer never materializes the whole node table the way
770
+ * `getAllNodes().filter(...)` did — that array alone OOM'd Node's default
771
+ * heap on a 2M-node graph (#1212).
772
+ */
773
+ *iterateNodesByLanguageWithDecorator(language, decorator) {
774
+ // Fresh statement per call — an iterator holds an open cursor (see
775
+ // iterateNodesByKind).
776
+ const stmt = this.db.prepare("SELECT * FROM nodes WHERE language = ? AND decorators LIKE '%' || ? || '%'");
777
+ for (const row of stmt.iterate(language, `"${decorator}"`)) {
778
+ yield rowToNode(row);
779
+ }
780
+ }
781
+ /**
782
+ * Distinct languages present in the files table. One indexed aggregate —
783
+ * lets the dynamic-edge synthesizers skip passes for languages the project
784
+ * doesn't contain at all (a Kotlin pass has no work on a pure-C repo), so
785
+ * their cost is zero rather than a full-graph scan that finds nothing (#1212).
786
+ */
787
+ getDistinctFileLanguages() {
788
+ const rows = this.db.prepare('SELECT DISTINCT language FROM files').all();
789
+ return new Set(rows.map((r) => r.language));
790
+ }
648
791
  /**
649
792
  * Get all nodes in the database
650
793
  */
@@ -1259,12 +1402,23 @@ class QueryBuilder {
1259
1402
  /**
1260
1403
  * Get incoming edges to a node
1261
1404
  */
1262
- getIncomingEdges(targetId, kinds) {
1405
+ getIncomingEdges(targetId, kinds, limit) {
1406
+ const cap = typeof limit === 'number' && Number.isFinite(limit)
1407
+ ? Math.max(1, Math.min(Math.floor(limit), 2000))
1408
+ : undefined;
1263
1409
  if (kinds && kinds.length > 0) {
1264
- const sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')})`;
1410
+ let sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')})`;
1411
+ if (cap !== undefined)
1412
+ sql += ` LIMIT ${cap}`;
1265
1413
  const rows = this.db.prepare(sql).all(targetId, ...kinds);
1266
1414
  return rows.map(rowToEdge);
1267
1415
  }
1416
+ if (cap !== undefined) {
1417
+ const rows = this.db
1418
+ .prepare('SELECT * FROM edges WHERE target = ? LIMIT ?')
1419
+ .all(targetId, cap);
1420
+ return rows.map(rowToEdge);
1421
+ }
1268
1422
  if (!this.stmts.getEdgesByTarget) {
1269
1423
  this.stmts.getEdgesByTarget = this.db.prepare('SELECT * FROM edges WHERE target = ?');
1270
1424
  }
@@ -1343,7 +1497,8 @@ class QueryBuilder {
1343
1497
  * {@link getDependentFilePaths}: all kinds except `contains`.
1344
1498
  */
1345
1499
  getCrossFileIncomingEdgesWithTarget(filePath) {
1346
- const sql = `SELECT e.*, tgt.name AS target_name, tgt.kind AS target_kind
1500
+ const sql = `SELECT e.*, tgt.name AS target_name, tgt.kind AS target_kind,
1501
+ src.file_path AS source_file_path, src.language AS source_language
1347
1502
  FROM edges e
1348
1503
  JOIN nodes tgt ON tgt.id = e.target
1349
1504
  JOIN nodes src ON src.id = e.source
@@ -1355,6 +1510,8 @@ class QueryBuilder {
1355
1510
  ...rowToEdge(row),
1356
1511
  targetName: row.target_name,
1357
1512
  targetKind: row.target_kind,
1513
+ sourceFilePath: row.source_file_path,
1514
+ sourceLanguage: row.source_language,
1358
1515
  }));
1359
1516
  }
1360
1517
  /**
@@ -1541,7 +1698,7 @@ class QueryBuilder {
1541
1698
  */
1542
1699
  getUnresolvedReferencesCount() {
1543
1700
  if (!this.stmts.getUnresolvedCount) {
1544
- this.stmts.getUnresolvedCount = this.db.prepare('SELECT COUNT(*) as count FROM unresolved_refs');
1701
+ this.stmts.getUnresolvedCount = this.db.prepare("SELECT COUNT(*) as count FROM unresolved_refs WHERE status = 'pending'");
1545
1702
  }
1546
1703
  const row = this.stmts.getUnresolvedCount.get();
1547
1704
  return row.count;
@@ -1552,7 +1709,7 @@ class QueryBuilder {
1552
1709
  */
1553
1710
  getUnresolvedReferencesBatch(offset, limit) {
1554
1711
  if (!this.stmts.getUnresolvedBatch) {
1555
- this.stmts.getUnresolvedBatch = this.db.prepare('SELECT * FROM unresolved_refs LIMIT ? OFFSET ?');
1712
+ this.stmts.getUnresolvedBatch = this.db.prepare("SELECT * FROM unresolved_refs WHERE status = 'pending' LIMIT ? OFFSET ?");
1556
1713
  }
1557
1714
  const rows = this.stmts.getUnresolvedBatch.all(limit, offset);
1558
1715
  return rows.map((row) => ({
@@ -1586,6 +1743,135 @@ class QueryBuilder {
1586
1743
  const rows = this.stmts.getAllNodeNames.all();
1587
1744
  return rows.map((r) => r.name);
1588
1745
  }
1746
+ /**
1747
+ * Stream distinct node names one row at a time — for resolver cache warm-up
1748
+ * that needs to yield to the event loop mid-scan.
1749
+ */
1750
+ *iterateNodeNames() {
1751
+ const stmt = this.db.prepare('SELECT DISTINCT name FROM nodes');
1752
+ for (const row of stmt.iterate()) {
1753
+ yield row.name;
1754
+ }
1755
+ }
1756
+ /**
1757
+ * Park refs as status='failed' so drain loops skip them while sync can retry
1758
+ * when a later file change introduces a matching symbol.
1759
+ */
1760
+ markReferencesFailed(refs) {
1761
+ if (refs.length === 0)
1762
+ return;
1763
+ const stmt = this.db.prepare("UPDATE unresolved_refs SET status = 'failed', name_tail = ? WHERE from_node_id = ? AND reference_name = ? AND reference_kind = ?");
1764
+ const markMany = this.db.transaction((items) => {
1765
+ for (const ref of items) {
1766
+ stmt.run(referenceNameTail(ref.referenceName), ref.fromNodeId, ref.referenceName, ref.referenceKind);
1767
+ }
1768
+ });
1769
+ markMany(refs);
1770
+ }
1771
+ /**
1772
+ * Delete unresolved-ref rows by row id — the precise cleanup for refs a
1773
+ * resolution pass actually processed. The key-tuple variant above also
1774
+ * deletes SIBLING rows (same caller calling the same callee at other lines)
1775
+ * that a later batch hasn't attempted yet, so when a batch boundary split a
1776
+ * caller's same-named call sites, the later sites' edges were silently never
1777
+ * created (#1269).
1778
+ */
1779
+ deleteReferencesByRowIds(rowIds) {
1780
+ if (rowIds.length === 0)
1781
+ return;
1782
+ for (let i = 0; i < rowIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1783
+ const chunk = rowIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1784
+ const placeholders = chunk.map(() => '?').join(',');
1785
+ this.db.prepare(`DELETE FROM unresolved_refs WHERE id IN (${placeholders})`).run(...chunk);
1786
+ }
1787
+ }
1788
+ /**
1789
+ * Park refs as status='failed' by row id — the precise counterpart of
1790
+ * markReferencesFailed, for the same reason as deleteReferencesByRowIds:
1791
+ * the key-tuple variant also flips same-key sibling rows in later batches
1792
+ * to 'failed' before they were ever attempted (#1269). Resolution outcome
1793
+ * can differ per call site (receiver-type inference reads the ref's line),
1794
+ * so a sibling must not inherit this row's failure.
1795
+ */
1796
+ markReferencesFailedByRowIds(refs) {
1797
+ if (refs.length === 0)
1798
+ return;
1799
+ const stmt = this.db.prepare("UPDATE unresolved_refs SET status = 'failed', name_tail = ? WHERE id = ?");
1800
+ const markMany = this.db.transaction((items) => {
1801
+ for (const ref of items) {
1802
+ stmt.run(referenceNameTail(ref.referenceName), ref.rowId);
1803
+ }
1804
+ });
1805
+ markMany(refs);
1806
+ }
1807
+ /**
1808
+ * Failed refs whose name tail matches one of the given symbol names — the
1809
+ * candidates a sync should retry after files carrying those names changed
1810
+ * (#1240). Names matching more than `perNameCeiling` failed refs are
1811
+ * skipped entirely: at that population a name is external/builtin noise
1812
+ * (`get`, `map`, …) that one new definition won't resolve — the same
1813
+ * rationale as resolution's AMBIGUOUS_NAME_CEILING (#999) — and retrying an
1814
+ * arbitrary subset would be both wasted work and incoherent coverage.
1815
+ */
1816
+ getRetryableFailedReferences(names, perNameCeiling = 500) {
1817
+ if (names.length === 0)
1818
+ return [];
1819
+ // Pass 1: per-tail counts, chunked under the SQLite parameter limit.
1820
+ const retryNames = [];
1821
+ for (let i = 0; i < names.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1822
+ const chunk = names.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1823
+ const placeholders = chunk.map(() => '?').join(',');
1824
+ const counts = this.db
1825
+ .prepare(`SELECT name_tail, COUNT(*) as count FROM unresolved_refs WHERE status = 'failed' AND name_tail IN (${placeholders}) GROUP BY name_tail`)
1826
+ .all(...chunk);
1827
+ for (const row of counts) {
1828
+ if (row.count <= perNameCeiling)
1829
+ retryNames.push(row.name_tail);
1830
+ }
1831
+ }
1832
+ if (retryNames.length === 0)
1833
+ return [];
1834
+ // Pass 2: load the surviving rows.
1835
+ const rows = [];
1836
+ for (let i = 0; i < retryNames.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1837
+ const chunk = retryNames.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1838
+ const placeholders = chunk.map(() => '?').join(',');
1839
+ const chunkRows = this.db
1840
+ .prepare(`SELECT * FROM unresolved_refs WHERE status = 'failed' AND name_tail IN (${placeholders})`)
1841
+ .all(...chunk);
1842
+ rows.push(...chunkRows);
1843
+ }
1844
+ return rows.map((row) => ({
1845
+ fromNodeId: row.from_node_id,
1846
+ referenceName: row.reference_name,
1847
+ referenceKind: row.reference_kind,
1848
+ line: row.line,
1849
+ column: row.col,
1850
+ candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
1851
+ filePath: row.file_path,
1852
+ language: row.language,
1853
+ rowId: row.id,
1854
+ }));
1855
+ }
1856
+ /**
1857
+ * Distinct node names present in the given files — the symbol names a sync
1858
+ * pass uses to look up retryable failed refs after those files changed.
1859
+ */
1860
+ getNodeNamesByFiles(filePaths) {
1861
+ if (filePaths.length === 0)
1862
+ return [];
1863
+ const names = new Set();
1864
+ for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1865
+ const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1866
+ const placeholders = chunk.map(() => '?').join(',');
1867
+ const rows = this.db
1868
+ .prepare(`SELECT DISTINCT name FROM nodes WHERE file_path IN (${placeholders})`)
1869
+ .all(...chunk);
1870
+ for (const row of rows)
1871
+ names.add(row.name);
1872
+ }
1873
+ return [...names];
1874
+ }
1589
1875
  /**
1590
1876
  * Get unresolved references scoped to specific file paths.
1591
1877
  * Uses the idx_unresolved_file_path index for efficient lookup.
@@ -1602,7 +1888,7 @@ class QueryBuilder {
1602
1888
  const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1603
1889
  const placeholders = chunk.map(() => '?').join(',');
1604
1890
  const chunkRows = this.db
1605
- .prepare(`SELECT * FROM unresolved_refs WHERE file_path IN (${placeholders})`)
1891
+ .prepare(`SELECT * FROM unresolved_refs WHERE status = 'pending' AND file_path IN (${placeholders})`)
1606
1892
  .all(...chunk);
1607
1893
  rows.push(...chunkRows);
1608
1894
  }
@@ -1752,6 +2038,13 @@ class QueryBuilder {
1752
2038
  .get(cacheKey);
1753
2039
  return row ?? null;
1754
2040
  }
2041
+ /** All cache keys — used to warm the in-memory key index after daemon restart. */
2042
+ listMcpQueryCacheKeys() {
2043
+ const rows = this.db
2044
+ .prepare('SELECT cache_key FROM mcp_query_cache')
2045
+ .all();
2046
+ return rows.map((r) => r.cache_key);
2047
+ }
1755
2048
  setMcpQueryCache(cacheKey, tool, response, createdAt) {
1756
2049
  this.db
1757
2050
  .prepare('INSERT INTO mcp_query_cache (cache_key, tool, response, created_at) VALUES (?, ?, ?, ?) ' +