@quolu/lattice 0.70.0 → 0.71.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 (603) hide show
  1. package/bin/lattice.mjs +21 -0
  2. package/package.json +1 -1
  3. package/sensor/dist/bin/lattice-sensor.js +746 -323
  4. package/sensor/dist/bin/viewer-gate.d.ts +17 -0
  5. package/sensor/dist/bin/viewer-gate.d.ts.map +1 -0
  6. package/sensor/dist/bin/viewer-gate.js +31 -0
  7. package/sensor/dist/bin/viewer-gate.js.map +1 -0
  8. package/sensor/dist/context/formatter.d.ts +7 -1
  9. package/sensor/dist/context/formatter.d.ts.map +1 -1
  10. package/sensor/dist/context/formatter.js +12 -6
  11. package/sensor/dist/context/formatter.js.map +1 -1
  12. package/sensor/dist/context/index.d.ts +12 -0
  13. package/sensor/dist/context/index.d.ts.map +1 -1
  14. package/sensor/dist/context/index.js +103 -16
  15. package/sensor/dist/context/index.js.map +1 -1
  16. package/sensor/dist/db/index.d.ts +23 -2
  17. package/sensor/dist/db/index.d.ts.map +1 -1
  18. package/sensor/dist/db/index.js +109 -31
  19. package/sensor/dist/db/index.js.map +1 -1
  20. package/sensor/dist/db/migrations.d.ts +1 -1
  21. package/sensor/dist/db/migrations.d.ts.map +1 -1
  22. package/sensor/dist/db/migrations.js +68 -1
  23. package/sensor/dist/db/migrations.js.map +1 -1
  24. package/sensor/dist/db/queries.d.ts +583 -6
  25. package/sensor/dist/db/queries.d.ts.map +1 -1
  26. package/sensor/dist/db/queries.js +1425 -57
  27. package/sensor/dist/db/queries.js.map +1 -1
  28. package/sensor/dist/db/schema.sql +27 -4
  29. package/sensor/dist/db/sqlite-adapter.d.ts +2 -0
  30. package/sensor/dist/db/sqlite-adapter.d.ts.map +1 -1
  31. package/sensor/dist/db/sqlite-adapter.js +56 -7
  32. package/sensor/dist/db/sqlite-adapter.js.map +1 -1
  33. package/sensor/dist/db/synthesis-stage.d.ts +12 -0
  34. package/sensor/dist/db/synthesis-stage.d.ts.map +1 -0
  35. package/sensor/dist/db/synthesis-stage.js +93 -0
  36. package/sensor/dist/db/synthesis-stage.js.map +1 -0
  37. package/sensor/dist/db/wal-valve.d.ts +32 -12
  38. package/sensor/dist/db/wal-valve.d.ts.map +1 -1
  39. package/sensor/dist/db/wal-valve.js +65 -22
  40. package/sensor/dist/db/wal-valve.js.map +1 -1
  41. package/sensor/dist/db/wsl-shared-index.d.ts +53 -0
  42. package/sensor/dist/db/wsl-shared-index.d.ts.map +1 -0
  43. package/sensor/dist/db/wsl-shared-index.js +121 -0
  44. package/sensor/dist/db/wsl-shared-index.js.map +1 -0
  45. package/sensor/dist/directory.d.ts +126 -2
  46. package/sensor/dist/directory.d.ts.map +1 -1
  47. package/sensor/dist/directory.js +285 -16
  48. package/sensor/dist/directory.js.map +1 -1
  49. package/sensor/dist/errors.d.ts +14 -0
  50. package/sensor/dist/errors.d.ts.map +1 -1
  51. package/sensor/dist/errors.js +16 -1
  52. package/sensor/dist/errors.js.map +1 -1
  53. package/sensor/dist/extraction/cfml-extractor.d.ts +1 -0
  54. package/sensor/dist/extraction/cfml-extractor.d.ts.map +1 -1
  55. package/sensor/dist/extraction/cfml-extractor.js +15 -3
  56. package/sensor/dist/extraction/cfml-extractor.js.map +1 -1
  57. package/sensor/dist/extraction/dfm-extractor.d.ts.map +1 -1
  58. package/sensor/dist/extraction/dfm-extractor.js +14 -10
  59. package/sensor/dist/extraction/dfm-extractor.js.map +1 -1
  60. package/sensor/dist/extraction/extraction-version.d.ts +1 -1
  61. package/sensor/dist/extraction/extraction-version.d.ts.map +1 -1
  62. package/sensor/dist/extraction/extraction-version.js +3 -1
  63. package/sensor/dist/extraction/extraction-version.js.map +1 -1
  64. package/sensor/dist/extraction/function-ref.d.ts +6 -4
  65. package/sensor/dist/extraction/function-ref.d.ts.map +1 -1
  66. package/sensor/dist/extraction/function-ref.js +35 -14
  67. package/sensor/dist/extraction/function-ref.js.map +1 -1
  68. package/sensor/dist/extraction/generated-detection.d.ts +48 -12
  69. package/sensor/dist/extraction/generated-detection.d.ts.map +1 -1
  70. package/sensor/dist/extraction/generated-detection.js +176 -11
  71. package/sensor/dist/extraction/generated-detection.js.map +1 -1
  72. package/sensor/dist/extraction/grammars.d.ts +38 -0
  73. package/sensor/dist/extraction/grammars.d.ts.map +1 -1
  74. package/sensor/dist/extraction/grammars.js +115 -3
  75. package/sensor/dist/extraction/grammars.js.map +1 -1
  76. package/sensor/dist/extraction/index.d.ts +218 -14
  77. package/sensor/dist/extraction/index.d.ts.map +1 -1
  78. package/sensor/dist/extraction/index.js +887 -175
  79. package/sensor/dist/extraction/index.js.map +1 -1
  80. package/sensor/dist/extraction/languages/c-cpp.d.ts +58 -0
  81. package/sensor/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  82. package/sensor/dist/extraction/languages/c-cpp.js +310 -19
  83. package/sensor/dist/extraction/languages/c-cpp.js.map +1 -1
  84. package/sensor/dist/extraction/languages/dart.d.ts.map +1 -1
  85. package/sensor/dist/extraction/languages/dart.js +8 -3
  86. package/sensor/dist/extraction/languages/dart.js.map +1 -1
  87. package/sensor/dist/extraction/languages/erlang.d.ts.map +1 -1
  88. package/sensor/dist/extraction/languages/erlang.js +58 -22
  89. package/sensor/dist/extraction/languages/erlang.js.map +1 -1
  90. package/sensor/dist/extraction/languages/javascript.js +1 -1
  91. package/sensor/dist/extraction/languages/javascript.js.map +1 -1
  92. package/sensor/dist/extraction/languages/kotlin.d.ts.map +1 -1
  93. package/sensor/dist/extraction/languages/kotlin.js +169 -36
  94. package/sensor/dist/extraction/languages/kotlin.js.map +1 -1
  95. package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
  96. package/sensor/dist/extraction/languages/lua.js +4 -1
  97. package/sensor/dist/extraction/languages/lua.js.map +1 -1
  98. package/sensor/dist/extraction/languages/objc.d.ts.map +1 -1
  99. package/sensor/dist/extraction/languages/objc.js +4 -0
  100. package/sensor/dist/extraction/languages/objc.js.map +1 -1
  101. package/sensor/dist/extraction/languages/python.d.ts.map +1 -1
  102. package/sensor/dist/extraction/languages/python.js +71 -0
  103. package/sensor/dist/extraction/languages/python.js.map +1 -1
  104. package/sensor/dist/extraction/languages/rust.d.ts +20 -0
  105. package/sensor/dist/extraction/languages/rust.d.ts.map +1 -1
  106. package/sensor/dist/extraction/languages/rust.js +50 -20
  107. package/sensor/dist/extraction/languages/rust.js.map +1 -1
  108. package/sensor/dist/extraction/languages/scala.d.ts.map +1 -1
  109. package/sensor/dist/extraction/languages/scala.js +18 -3
  110. package/sensor/dist/extraction/languages/scala.js.map +1 -1
  111. package/sensor/dist/extraction/languages/swift.d.ts +15 -0
  112. package/sensor/dist/extraction/languages/swift.d.ts.map +1 -1
  113. package/sensor/dist/extraction/languages/swift.js +50 -0
  114. package/sensor/dist/extraction/languages/swift.js.map +1 -1
  115. package/sensor/dist/extraction/languages/typescript.d.ts.map +1 -1
  116. package/sensor/dist/extraction/languages/typescript.js +9 -2
  117. package/sensor/dist/extraction/languages/typescript.js.map +1 -1
  118. package/sensor/dist/extraction/liquid-extractor.d.ts +18 -0
  119. package/sensor/dist/extraction/liquid-extractor.d.ts.map +1 -1
  120. package/sensor/dist/extraction/liquid-extractor.js +98 -22
  121. package/sensor/dist/extraction/liquid-extractor.js.map +1 -1
  122. package/sensor/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  123. package/sensor/dist/extraction/mybatis-extractor.js +6 -4
  124. package/sensor/dist/extraction/mybatis-extractor.js.map +1 -1
  125. package/sensor/dist/extraction/parse-pool.d.ts +7 -0
  126. package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
  127. package/sensor/dist/extraction/parse-pool.js +20 -1
  128. package/sensor/dist/extraction/parse-pool.js.map +1 -1
  129. package/sensor/dist/extraction/syntax-tokens.d.ts +103 -0
  130. package/sensor/dist/extraction/syntax-tokens.d.ts.map +1 -0
  131. package/sensor/dist/extraction/syntax-tokens.js +381 -0
  132. package/sensor/dist/extraction/syntax-tokens.js.map +1 -0
  133. package/sensor/dist/extraction/tree-sitter-helpers.d.ts +5 -0
  134. package/sensor/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  135. package/sensor/dist/extraction/tree-sitter-helpers.js +17 -0
  136. package/sensor/dist/extraction/tree-sitter-helpers.js.map +1 -1
  137. package/sensor/dist/extraction/tree-sitter-types.d.ts +31 -3
  138. package/sensor/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  139. package/sensor/dist/extraction/tree-sitter.d.ts +101 -18
  140. package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
  141. package/sensor/dist/extraction/tree-sitter.js +928 -134
  142. package/sensor/dist/extraction/tree-sitter.js.map +1 -1
  143. package/sensor/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  144. package/sensor/dist/file-limits.d.ts +40 -0
  145. package/sensor/dist/file-limits.d.ts.map +1 -0
  146. package/sensor/dist/file-limits.js +147 -0
  147. package/sensor/dist/file-limits.js.map +1 -0
  148. package/sensor/dist/graph/branch-guards.d.ts +265 -0
  149. package/sensor/dist/graph/branch-guards.d.ts.map +1 -0
  150. package/sensor/dist/graph/branch-guards.js +2325 -0
  151. package/sensor/dist/graph/branch-guards.js.map +1 -0
  152. package/sensor/dist/graph/dead-code.d.ts +251 -0
  153. package/sensor/dist/graph/dead-code.d.ts.map +1 -0
  154. package/sensor/dist/graph/dead-code.js +776 -0
  155. package/sensor/dist/graph/dead-code.js.map +1 -0
  156. package/sensor/dist/graph/dynamic-boundary-report.d.ts +121 -0
  157. package/sensor/dist/graph/dynamic-boundary-report.d.ts.map +1 -0
  158. package/sensor/dist/graph/dynamic-boundary-report.js +283 -0
  159. package/sensor/dist/graph/dynamic-boundary-report.js.map +1 -0
  160. package/sensor/dist/graph/index.d.ts +4 -0
  161. package/sensor/dist/graph/index.d.ts.map +1 -1
  162. package/sensor/dist/graph/index.js +16 -1
  163. package/sensor/dist/graph/index.js.map +1 -1
  164. package/sensor/dist/graph/named-symbol-flow.d.ts +125 -0
  165. package/sensor/dist/graph/named-symbol-flow.d.ts.map +1 -0
  166. package/sensor/dist/graph/named-symbol-flow.js +552 -0
  167. package/sensor/dist/graph/named-symbol-flow.js.map +1 -0
  168. package/sensor/dist/graph/queries.d.ts.map +1 -1
  169. package/sensor/dist/graph/queries.js +2 -0
  170. package/sensor/dist/graph/queries.js.map +1 -1
  171. package/sensor/dist/graph/symbol-lookup.d.ts +83 -0
  172. package/sensor/dist/graph/symbol-lookup.d.ts.map +1 -0
  173. package/sensor/dist/graph/symbol-lookup.js +181 -0
  174. package/sensor/dist/graph/symbol-lookup.js.map +1 -0
  175. package/sensor/dist/graph/traversal.d.ts +9 -0
  176. package/sensor/dist/graph/traversal.d.ts.map +1 -1
  177. package/sensor/dist/graph/traversal.js +62 -36
  178. package/sensor/dist/graph/traversal.js.map +1 -1
  179. package/sensor/dist/graph/type-hierarchy.d.ts +155 -0
  180. package/sensor/dist/graph/type-hierarchy.d.ts.map +1 -0
  181. package/sensor/dist/graph/type-hierarchy.js +382 -0
  182. package/sensor/dist/graph/type-hierarchy.js.map +1 -0
  183. package/sensor/dist/index.d.ts +272 -10
  184. package/sensor/dist/index.d.ts.map +1 -1
  185. package/sensor/dist/index.js +510 -34
  186. package/sensor/dist/index.js.map +1 -1
  187. package/sensor/dist/mcp/answer-freshness.d.ts +10 -0
  188. package/sensor/dist/mcp/answer-freshness.d.ts.map +1 -0
  189. package/sensor/dist/mcp/answer-freshness.js +68 -0
  190. package/sensor/dist/mcp/answer-freshness.js.map +1 -0
  191. package/sensor/dist/mcp/daemon-manager.d.ts +1 -1
  192. package/sensor/dist/mcp/daemon-manager.d.ts.map +1 -1
  193. package/sensor/dist/mcp/daemon-manager.js +17 -1
  194. package/sensor/dist/mcp/daemon-manager.js.map +1 -1
  195. package/sensor/dist/mcp/daemon-paths.d.ts +8 -0
  196. package/sensor/dist/mcp/daemon-paths.d.ts.map +1 -1
  197. package/sensor/dist/mcp/daemon-paths.js +65 -2
  198. package/sensor/dist/mcp/daemon-paths.js.map +1 -1
  199. package/sensor/dist/mcp/daemon-registry.d.ts +17 -4
  200. package/sensor/dist/mcp/daemon-registry.d.ts.map +1 -1
  201. package/sensor/dist/mcp/daemon-registry.js +133 -23
  202. package/sensor/dist/mcp/daemon-registry.js.map +1 -1
  203. package/sensor/dist/mcp/daemon.d.ts +26 -13
  204. package/sensor/dist/mcp/daemon.d.ts.map +1 -1
  205. package/sensor/dist/mcp/daemon.js +126 -32
  206. package/sensor/dist/mcp/daemon.js.map +1 -1
  207. package/sensor/dist/mcp/dynamic-boundaries.d.ts +1 -10
  208. package/sensor/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  209. package/sensor/dist/mcp/dynamic-boundaries.js +46 -42
  210. package/sensor/dist/mcp/dynamic-boundaries.js.map +1 -1
  211. package/sensor/dist/mcp/engine.d.ts +31 -9
  212. package/sensor/dist/mcp/engine.d.ts.map +1 -1
  213. package/sensor/dist/mcp/engine.js +190 -79
  214. package/sensor/dist/mcp/engine.js.map +1 -1
  215. package/sensor/dist/mcp/explore-dedup.d.ts +140 -0
  216. package/sensor/dist/mcp/explore-dedup.d.ts.map +1 -0
  217. package/sensor/dist/mcp/explore-dedup.js +239 -0
  218. package/sensor/dist/mcp/explore-dedup.js.map +1 -0
  219. package/sensor/dist/mcp/explore-diagnostics.d.ts +289 -0
  220. package/sensor/dist/mcp/explore-diagnostics.d.ts.map +1 -0
  221. package/sensor/dist/mcp/explore-diagnostics.js +558 -0
  222. package/sensor/dist/mcp/explore-diagnostics.js.map +1 -0
  223. package/sensor/dist/mcp/explore-session-state.d.ts +217 -0
  224. package/sensor/dist/mcp/explore-session-state.d.ts.map +1 -0
  225. package/sensor/dist/mcp/explore-session-state.js +322 -0
  226. package/sensor/dist/mcp/explore-session-state.js.map +1 -0
  227. package/sensor/dist/mcp/index-freshness-worker.d.ts +2 -0
  228. package/sensor/dist/mcp/index-freshness-worker.d.ts.map +1 -0
  229. package/sensor/dist/mcp/index-freshness-worker.js +30 -0
  230. package/sensor/dist/mcp/index-freshness-worker.js.map +1 -0
  231. package/sensor/dist/mcp/index-freshness.d.ts +7 -0
  232. package/sensor/dist/mcp/index-freshness.d.ts.map +1 -0
  233. package/sensor/dist/mcp/index-freshness.js +94 -0
  234. package/sensor/dist/mcp/index-freshness.js.map +1 -0
  235. package/sensor/dist/mcp/index.d.ts +3 -1
  236. package/sensor/dist/mcp/index.d.ts.map +1 -1
  237. package/sensor/dist/mcp/index.js +109 -12
  238. package/sensor/dist/mcp/index.js.map +1 -1
  239. package/sensor/dist/mcp/liveness-watchdog.d.ts +4 -1
  240. package/sensor/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  241. package/sensor/dist/mcp/liveness-watchdog.js +11 -2
  242. package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
  243. package/sensor/dist/mcp/project-lifecycle.d.ts +9 -0
  244. package/sensor/dist/mcp/project-lifecycle.d.ts.map +1 -0
  245. package/sensor/dist/mcp/project-lifecycle.js +182 -0
  246. package/sensor/dist/mcp/project-lifecycle.js.map +1 -0
  247. package/sensor/dist/mcp/proxy.d.ts.map +1 -1
  248. package/sensor/dist/mcp/proxy.js +15 -1
  249. package/sensor/dist/mcp/proxy.js.map +1 -1
  250. package/sensor/dist/mcp/query-pool.d.ts +2 -2
  251. package/sensor/dist/mcp/query-pool.d.ts.map +1 -1
  252. package/sensor/dist/mcp/query-pool.js +9 -7
  253. package/sensor/dist/mcp/query-pool.js.map +1 -1
  254. package/sensor/dist/mcp/query-worker.js +2 -2
  255. package/sensor/dist/mcp/query-worker.js.map +1 -1
  256. package/sensor/dist/mcp/server-instructions.d.ts +2 -2
  257. package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
  258. package/sensor/dist/mcp/server-instructions.js +13 -6
  259. package/sensor/dist/mcp/server-instructions.js.map +1 -1
  260. package/sensor/dist/mcp/session.d.ts +16 -0
  261. package/sensor/dist/mcp/session.d.ts.map +1 -1
  262. package/sensor/dist/mcp/session.js +36 -13
  263. package/sensor/dist/mcp/session.js.map +1 -1
  264. package/sensor/dist/mcp/startup-handshake.d.ts +3 -1
  265. package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
  266. package/sensor/dist/mcp/startup-handshake.js +10 -2
  267. package/sensor/dist/mcp/startup-handshake.js.map +1 -1
  268. package/sensor/dist/mcp/tools.d.ts +496 -38
  269. package/sensor/dist/mcp/tools.d.ts.map +1 -1
  270. package/sensor/dist/mcp/tools.js +4021 -772
  271. package/sensor/dist/mcp/tools.js.map +1 -1
  272. package/sensor/dist/mcp/writer-lock.d.ts +61 -0
  273. package/sensor/dist/mcp/writer-lock.d.ts.map +1 -0
  274. package/sensor/dist/mcp/writer-lock.js +260 -0
  275. package/sensor/dist/mcp/writer-lock.js.map +1 -0
  276. package/sensor/dist/project-config.d.ts +23 -0
  277. package/sensor/dist/project-config.d.ts.map +1 -1
  278. package/sensor/dist/project-config.js +39 -2
  279. package/sensor/dist/project-config.js.map +1 -1
  280. package/sensor/dist/resolution/alias-binding.d.ts +56 -0
  281. package/sensor/dist/resolution/alias-binding.d.ts.map +1 -0
  282. package/sensor/dist/resolution/alias-binding.js +127 -0
  283. package/sensor/dist/resolution/alias-binding.js.map +1 -0
  284. package/sensor/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  285. package/sensor/dist/resolution/c-fnptr-synthesizer.js +21 -19
  286. package/sensor/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  287. package/sensor/dist/resolution/callback-synthesizer.d.ts +23 -1
  288. package/sensor/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  289. package/sensor/dist/resolution/callback-synthesizer.js +356 -155
  290. package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
  291. package/sensor/dist/resolution/cpp-constructor.d.ts +4 -0
  292. package/sensor/dist/resolution/cpp-constructor.d.ts.map +1 -0
  293. package/sensor/dist/resolution/cpp-constructor.js +123 -0
  294. package/sensor/dist/resolution/cpp-constructor.js.map +1 -0
  295. package/sensor/dist/resolution/cpp-macro-visibility.d.ts +12 -0
  296. package/sensor/dist/resolution/cpp-macro-visibility.d.ts.map +1 -0
  297. package/sensor/dist/resolution/cpp-macro-visibility.js +342 -0
  298. package/sensor/dist/resolution/cpp-macro-visibility.js.map +1 -0
  299. package/sensor/dist/resolution/expo-router-synthesizer.d.ts +32 -0
  300. package/sensor/dist/resolution/expo-router-synthesizer.d.ts.map +1 -0
  301. package/sensor/dist/resolution/expo-router-synthesizer.js +189 -0
  302. package/sensor/dist/resolution/expo-router-synthesizer.js.map +1 -0
  303. package/sensor/dist/resolution/frameworks/csharp.d.ts.map +1 -1
  304. package/sensor/dist/resolution/frameworks/csharp.js +96 -2
  305. package/sensor/dist/resolution/frameworks/csharp.js.map +1 -1
  306. package/sensor/dist/resolution/frameworks/expo-router.d.ts +189 -0
  307. package/sensor/dist/resolution/frameworks/expo-router.d.ts.map +1 -0
  308. package/sensor/dist/resolution/frameworks/expo-router.js +785 -0
  309. package/sensor/dist/resolution/frameworks/expo-router.js.map +1 -0
  310. package/sensor/dist/resolution/frameworks/express.d.ts.map +1 -1
  311. package/sensor/dist/resolution/frameworks/express.js +251 -16
  312. package/sensor/dist/resolution/frameworks/express.js.map +1 -1
  313. package/sensor/dist/resolution/frameworks/index.d.ts +5 -0
  314. package/sensor/dist/resolution/frameworks/index.d.ts.map +1 -1
  315. package/sensor/dist/resolution/frameworks/index.js +29 -1
  316. package/sensor/dist/resolution/frameworks/index.js.map +1 -1
  317. package/sensor/dist/resolution/frameworks/java.d.ts.map +1 -1
  318. package/sensor/dist/resolution/frameworks/java.js +79 -47
  319. package/sensor/dist/resolution/frameworks/java.js.map +1 -1
  320. package/sensor/dist/resolution/frameworks/laravel.d.ts.map +1 -1
  321. package/sensor/dist/resolution/frameworks/laravel.js +294 -4
  322. package/sensor/dist/resolution/frameworks/laravel.js.map +1 -1
  323. package/sensor/dist/resolution/frameworks/nestjs.d.ts.map +1 -1
  324. package/sensor/dist/resolution/frameworks/nestjs.js +6 -14
  325. package/sensor/dist/resolution/frameworks/nestjs.js.map +1 -1
  326. package/sensor/dist/resolution/frameworks/nextjs.d.ts +76 -0
  327. package/sensor/dist/resolution/frameworks/nextjs.d.ts.map +1 -0
  328. package/sensor/dist/resolution/frameworks/nextjs.js +328 -0
  329. package/sensor/dist/resolution/frameworks/nextjs.js.map +1 -0
  330. package/sensor/dist/resolution/frameworks/object-literal.d.ts +32 -0
  331. package/sensor/dist/resolution/frameworks/object-literal.d.ts.map +1 -0
  332. package/sensor/dist/resolution/frameworks/object-literal.js +139 -0
  333. package/sensor/dist/resolution/frameworks/object-literal.js.map +1 -0
  334. package/sensor/dist/resolution/frameworks/package-deps.d.ts +14 -0
  335. package/sensor/dist/resolution/frameworks/package-deps.d.ts.map +1 -0
  336. package/sensor/dist/resolution/frameworks/package-deps.js +78 -0
  337. package/sensor/dist/resolution/frameworks/package-deps.js.map +1 -0
  338. package/sensor/dist/resolution/frameworks/python.d.ts.map +1 -1
  339. package/sensor/dist/resolution/frameworks/python.js +166 -0
  340. package/sensor/dist/resolution/frameworks/python.js.map +1 -1
  341. package/sensor/dist/resolution/frameworks/react-native.d.ts +26 -0
  342. package/sensor/dist/resolution/frameworks/react-native.d.ts.map +1 -1
  343. package/sensor/dist/resolution/frameworks/react-native.js +153 -14
  344. package/sensor/dist/resolution/frameworks/react-native.js.map +1 -1
  345. package/sensor/dist/resolution/frameworks/react-router.d.ts +46 -0
  346. package/sensor/dist/resolution/frameworks/react-router.d.ts.map +1 -0
  347. package/sensor/dist/resolution/frameworks/react-router.js +187 -0
  348. package/sensor/dist/resolution/frameworks/react-router.js.map +1 -0
  349. package/sensor/dist/resolution/frameworks/react.d.ts +2 -1
  350. package/sensor/dist/resolution/frameworks/react.d.ts.map +1 -1
  351. package/sensor/dist/resolution/frameworks/react.js +188 -139
  352. package/sensor/dist/resolution/frameworks/react.js.map +1 -1
  353. package/sensor/dist/resolution/frameworks/svelte.d.ts.map +1 -1
  354. package/sensor/dist/resolution/frameworks/svelte.js +6 -1
  355. package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
  356. package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts +36 -0
  357. package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts.map +1 -0
  358. package/sensor/dist/resolution/frameworks/sveltekit-router.js +156 -0
  359. package/sensor/dist/resolution/frameworks/sveltekit-router.js.map +1 -0
  360. package/sensor/dist/resolution/frameworks/swift.d.ts.map +1 -1
  361. package/sensor/dist/resolution/frameworks/swift.js +140 -99
  362. package/sensor/dist/resolution/frameworks/swift.js.map +1 -1
  363. package/sensor/dist/resolution/frameworks/tanstack-router.d.ts +84 -0
  364. package/sensor/dist/resolution/frameworks/tanstack-router.d.ts.map +1 -0
  365. package/sensor/dist/resolution/frameworks/tanstack-router.js +437 -0
  366. package/sensor/dist/resolution/frameworks/tanstack-router.js.map +1 -0
  367. package/sensor/dist/resolution/frameworks/vue-router.d.ts +82 -0
  368. package/sensor/dist/resolution/frameworks/vue-router.d.ts.map +1 -0
  369. package/sensor/dist/resolution/frameworks/vue-router.js +377 -0
  370. package/sensor/dist/resolution/frameworks/vue-router.js.map +1 -0
  371. package/sensor/dist/resolution/import-resolver.d.ts +38 -0
  372. package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
  373. package/sensor/dist/resolution/import-resolver.js +394 -28
  374. package/sensor/dist/resolution/import-resolver.js.map +1 -1
  375. package/sensor/dist/resolution/index.d.ts +89 -25
  376. package/sensor/dist/resolution/index.d.ts.map +1 -1
  377. package/sensor/dist/resolution/index.js +573 -148
  378. package/sensor/dist/resolution/index.js.map +1 -1
  379. package/sensor/dist/resolution/js-builtins.d.ts +13 -0
  380. package/sensor/dist/resolution/js-builtins.d.ts.map +1 -0
  381. package/sensor/dist/resolution/js-builtins.js +46 -0
  382. package/sensor/dist/resolution/js-builtins.js.map +1 -0
  383. package/sensor/dist/resolution/name-matcher.d.ts +99 -22
  384. package/sensor/dist/resolution/name-matcher.d.ts.map +1 -1
  385. package/sensor/dist/resolution/name-matcher.js +2220 -86
  386. package/sensor/dist/resolution/name-matcher.js.map +1 -1
  387. package/sensor/dist/resolution/next-router-synthesizer.d.ts +27 -0
  388. package/sensor/dist/resolution/next-router-synthesizer.d.ts.map +1 -0
  389. package/sensor/dist/resolution/next-router-synthesizer.js +113 -0
  390. package/sensor/dist/resolution/next-router-synthesizer.js.map +1 -0
  391. package/sensor/dist/resolution/path-aliases.d.ts +6 -4
  392. package/sensor/dist/resolution/path-aliases.d.ts.map +1 -1
  393. package/sensor/dist/resolution/path-aliases.js +125 -17
  394. package/sensor/dist/resolution/path-aliases.js.map +1 -1
  395. package/sensor/dist/resolution/react-router-synthesizer.d.ts +33 -0
  396. package/sensor/dist/resolution/react-router-synthesizer.d.ts.map +1 -0
  397. package/sensor/dist/resolution/react-router-synthesizer.js +126 -0
  398. package/sensor/dist/resolution/react-router-synthesizer.js.map +1 -0
  399. package/sensor/dist/resolution/resolver-pool.d.ts +2 -0
  400. package/sensor/dist/resolution/resolver-pool.d.ts.map +1 -1
  401. package/sensor/dist/resolution/resolver-pool.js +4 -0
  402. package/sensor/dist/resolution/resolver-pool.js.map +1 -1
  403. package/sensor/dist/resolution/strip-comments.d.ts +6 -0
  404. package/sensor/dist/resolution/strip-comments.d.ts.map +1 -1
  405. package/sensor/dist/resolution/strip-comments.js +58 -0
  406. package/sensor/dist/resolution/strip-comments.js.map +1 -1
  407. package/sensor/dist/resolution/sveltekit-synthesizer.d.ts +58 -0
  408. package/sensor/dist/resolution/sveltekit-synthesizer.d.ts.map +1 -0
  409. package/sensor/dist/resolution/sveltekit-synthesizer.js +173 -0
  410. package/sensor/dist/resolution/sveltekit-synthesizer.js.map +1 -0
  411. package/sensor/dist/resolution/swift-type-visibility.d.ts +52 -0
  412. package/sensor/dist/resolution/swift-type-visibility.d.ts.map +1 -0
  413. package/sensor/dist/resolution/swift-type-visibility.js +328 -0
  414. package/sensor/dist/resolution/swift-type-visibility.js.map +1 -0
  415. package/sensor/dist/resolution/synth-utils.d.ts +26 -0
  416. package/sensor/dist/resolution/synth-utils.d.ts.map +1 -0
  417. package/sensor/dist/resolution/synth-utils.js +75 -0
  418. package/sensor/dist/resolution/synth-utils.js.map +1 -0
  419. package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts +31 -0
  420. package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts.map +1 -0
  421. package/sensor/dist/resolution/tanstack-router-synthesizer.js +114 -0
  422. package/sensor/dist/resolution/tanstack-router-synthesizer.js.map +1 -0
  423. package/sensor/dist/resolution/tier-synthesizer.d.ts +54 -0
  424. package/sensor/dist/resolution/tier-synthesizer.d.ts.map +1 -0
  425. package/sensor/dist/resolution/tier-synthesizer.js +1294 -0
  426. package/sensor/dist/resolution/tier-synthesizer.js.map +1 -0
  427. package/sensor/dist/resolution/types.d.ts +78 -2
  428. package/sensor/dist/resolution/types.d.ts.map +1 -1
  429. package/sensor/dist/resolution/types.js +64 -0
  430. package/sensor/dist/resolution/types.js.map +1 -1
  431. package/sensor/dist/resolution/vue-router-synthesizer.d.ts +30 -0
  432. package/sensor/dist/resolution/vue-router-synthesizer.d.ts.map +1 -0
  433. package/sensor/dist/resolution/vue-router-synthesizer.js +115 -0
  434. package/sensor/dist/resolution/vue-router-synthesizer.js.map +1 -0
  435. package/sensor/dist/search/identifier-segments.d.ts +10 -0
  436. package/sensor/dist/search/identifier-segments.d.ts.map +1 -1
  437. package/sensor/dist/search/identifier-segments.js +29 -0
  438. package/sensor/dist/search/identifier-segments.js.map +1 -1
  439. package/sensor/dist/search/query-paths.d.ts +106 -0
  440. package/sensor/dist/search/query-paths.d.ts.map +1 -0
  441. package/sensor/dist/search/query-paths.js +513 -0
  442. package/sensor/dist/search/query-paths.js.map +1 -0
  443. package/sensor/dist/search/query-utils.d.ts +22 -2
  444. package/sensor/dist/search/query-utils.d.ts.map +1 -1
  445. package/sensor/dist/search/query-utils.js +56 -16
  446. package/sensor/dist/search/query-utils.js.map +1 -1
  447. package/sensor/dist/sync/watch-policy.d.ts +14 -0
  448. package/sensor/dist/sync/watch-policy.d.ts.map +1 -1
  449. package/sensor/dist/sync/watch-policy.js +19 -0
  450. package/sensor/dist/sync/watch-policy.js.map +1 -1
  451. package/sensor/dist/sync/watcher.d.ts +74 -35
  452. package/sensor/dist/sync/watcher.d.ts.map +1 -1
  453. package/sensor/dist/sync/watcher.js +281 -99
  454. package/sensor/dist/sync/watcher.js.map +1 -1
  455. package/sensor/dist/telemetry/index.d.ts +4 -2
  456. package/sensor/dist/telemetry/index.d.ts.map +1 -1
  457. package/sensor/dist/telemetry/index.js +92 -41
  458. package/sensor/dist/telemetry/index.js.map +1 -1
  459. package/sensor/dist/types.d.ts +18 -2
  460. package/sensor/dist/types.d.ts.map +1 -1
  461. package/sensor/dist/types.js +2 -0
  462. package/sensor/dist/types.js.map +1 -1
  463. package/sensor/dist/ui-server/api/deadcode.d.ts +84 -0
  464. package/sensor/dist/ui-server/api/deadcode.d.ts.map +1 -0
  465. package/sensor/dist/ui-server/api/deadcode.js +136 -0
  466. package/sensor/dist/ui-server/api/deadcode.js.map +1 -0
  467. package/sensor/dist/ui-server/api/effects.d.ts +94 -0
  468. package/sensor/dist/ui-server/api/effects.d.ts.map +1 -0
  469. package/sensor/dist/ui-server/api/effects.js +527 -0
  470. package/sensor/dist/ui-server/api/effects.js.map +1 -0
  471. package/sensor/dist/ui-server/api/entrypoints.d.ts +92 -0
  472. package/sensor/dist/ui-server/api/entrypoints.d.ts.map +1 -0
  473. package/sensor/dist/ui-server/api/entrypoints.js +361 -0
  474. package/sensor/dist/ui-server/api/entrypoints.js.map +1 -0
  475. package/sensor/dist/ui-server/api/events.d.ts +183 -0
  476. package/sensor/dist/ui-server/api/events.d.ts.map +1 -0
  477. package/sensor/dist/ui-server/api/events.js +430 -0
  478. package/sensor/dist/ui-server/api/events.js.map +1 -0
  479. package/sensor/dist/ui-server/api/file.d.ts +66 -0
  480. package/sensor/dist/ui-server/api/file.d.ts.map +1 -0
  481. package/sensor/dist/ui-server/api/file.js +235 -0
  482. package/sensor/dist/ui-server/api/file.js.map +1 -0
  483. package/sensor/dist/ui-server/api/filecode.d.ts +126 -0
  484. package/sensor/dist/ui-server/api/filecode.d.ts.map +1 -0
  485. package/sensor/dist/ui-server/api/filecode.js +205 -0
  486. package/sensor/dist/ui-server/api/filecode.js.map +1 -0
  487. package/sensor/dist/ui-server/api/flow.d.ts +226 -0
  488. package/sensor/dist/ui-server/api/flow.d.ts.map +1 -0
  489. package/sensor/dist/ui-server/api/flow.js +612 -0
  490. package/sensor/dist/ui-server/api/flow.js.map +1 -0
  491. package/sensor/dist/ui-server/api/hierarchy.d.ts +81 -0
  492. package/sensor/dist/ui-server/api/hierarchy.d.ts.map +1 -0
  493. package/sensor/dist/ui-server/api/hierarchy.js +92 -0
  494. package/sensor/dist/ui-server/api/hierarchy.js.map +1 -0
  495. package/sensor/dist/ui-server/api/index.d.ts +90 -0
  496. package/sensor/dist/ui-server/api/index.d.ts.map +1 -0
  497. package/sensor/dist/ui-server/api/index.js +332 -0
  498. package/sensor/dist/ui-server/api/index.js.map +1 -0
  499. package/sensor/dist/ui-server/api/map.d.ts +256 -0
  500. package/sensor/dist/ui-server/api/map.d.ts.map +1 -0
  501. package/sensor/dist/ui-server/api/map.js +760 -0
  502. package/sensor/dist/ui-server/api/map.js.map +1 -0
  503. package/sensor/dist/ui-server/api/node.d.ts +81 -0
  504. package/sensor/dist/ui-server/api/node.d.ts.map +1 -0
  505. package/sensor/dist/ui-server/api/node.js +388 -0
  506. package/sensor/dist/ui-server/api/node.js.map +1 -0
  507. package/sensor/dist/ui-server/api/nodes.d.ts +25 -0
  508. package/sensor/dist/ui-server/api/nodes.d.ts.map +1 -0
  509. package/sensor/dist/ui-server/api/nodes.js +45 -0
  510. package/sensor/dist/ui-server/api/nodes.js.map +1 -0
  511. package/sensor/dist/ui-server/api/program.d.ts +139 -0
  512. package/sensor/dist/ui-server/api/program.d.ts.map +1 -0
  513. package/sensor/dist/ui-server/api/program.js +315 -0
  514. package/sensor/dist/ui-server/api/program.js.map +1 -0
  515. package/sensor/dist/ui-server/api/respond.d.ts +76 -0
  516. package/sensor/dist/ui-server/api/respond.d.ts.map +1 -0
  517. package/sensor/dist/ui-server/api/respond.js +186 -0
  518. package/sensor/dist/ui-server/api/respond.js.map +1 -0
  519. package/sensor/dist/ui-server/api/route-roots.d.ts +39 -0
  520. package/sensor/dist/ui-server/api/route-roots.d.ts.map +1 -0
  521. package/sensor/dist/ui-server/api/route-roots.js +96 -0
  522. package/sensor/dist/ui-server/api/route-roots.js.map +1 -0
  523. package/sensor/dist/ui-server/api/routes.d.ts +59 -0
  524. package/sensor/dist/ui-server/api/routes.d.ts.map +1 -0
  525. package/sensor/dist/ui-server/api/routes.js +108 -0
  526. package/sensor/dist/ui-server/api/routes.js.map +1 -0
  527. package/sensor/dist/ui-server/api/screens.d.ts +117 -0
  528. package/sensor/dist/ui-server/api/screens.d.ts.map +1 -0
  529. package/sensor/dist/ui-server/api/screens.js +611 -0
  530. package/sensor/dist/ui-server/api/screens.js.map +1 -0
  531. package/sensor/dist/ui-server/api/search.d.ts +29 -0
  532. package/sensor/dist/ui-server/api/search.d.ts.map +1 -0
  533. package/sensor/dist/ui-server/api/search.js +232 -0
  534. package/sensor/dist/ui-server/api/search.js.map +1 -0
  535. package/sensor/dist/ui-server/api/session.d.ts +52 -0
  536. package/sensor/dist/ui-server/api/session.d.ts.map +1 -0
  537. package/sensor/dist/ui-server/api/session.js +196 -0
  538. package/sensor/dist/ui-server/api/session.js.map +1 -0
  539. package/sensor/dist/ui-server/api/source.d.ts +200 -0
  540. package/sensor/dist/ui-server/api/source.d.ts.map +1 -0
  541. package/sensor/dist/ui-server/api/source.js +422 -0
  542. package/sensor/dist/ui-server/api/source.js.map +1 -0
  543. package/sensor/dist/ui-server/api/stats.d.ts +27 -0
  544. package/sensor/dist/ui-server/api/stats.d.ts.map +1 -0
  545. package/sensor/dist/ui-server/api/stats.js +164 -0
  546. package/sensor/dist/ui-server/api/stats.js.map +1 -0
  547. package/sensor/dist/ui-server/api/steps.d.ts +274 -0
  548. package/sensor/dist/ui-server/api/steps.d.ts.map +1 -0
  549. package/sensor/dist/ui-server/api/steps.js +1544 -0
  550. package/sensor/dist/ui-server/api/steps.js.map +1 -0
  551. package/sensor/dist/ui-server/api/trail-store.d.ts +142 -0
  552. package/sensor/dist/ui-server/api/trail-store.d.ts.map +1 -0
  553. package/sensor/dist/ui-server/api/trail-store.js +325 -0
  554. package/sensor/dist/ui-server/api/trail-store.js.map +1 -0
  555. package/sensor/dist/ui-server/api/trails.d.ts +163 -0
  556. package/sensor/dist/ui-server/api/trails.d.ts.map +1 -0
  557. package/sensor/dist/ui-server/api/trails.js +369 -0
  558. package/sensor/dist/ui-server/api/trails.js.map +1 -0
  559. package/sensor/dist/ui-server/api/when.d.ts +133 -0
  560. package/sensor/dist/ui-server/api/when.d.ts.map +1 -0
  561. package/sensor/dist/ui-server/api/when.js +275 -0
  562. package/sensor/dist/ui-server/api/when.js.map +1 -0
  563. package/sensor/dist/ui-server/api/wire.d.ts +179 -0
  564. package/sensor/dist/ui-server/api/wire.d.ts.map +1 -0
  565. package/sensor/dist/ui-server/api/wire.js +231 -0
  566. package/sensor/dist/ui-server/api/wire.js.map +1 -0
  567. package/sensor/dist/ui-server/assets.d.ts +40 -0
  568. package/sensor/dist/ui-server/assets.d.ts.map +1 -0
  569. package/sensor/dist/ui-server/assets.js +110 -0
  570. package/sensor/dist/ui-server/assets.js.map +1 -0
  571. package/sensor/dist/ui-server/constants.d.ts +32 -0
  572. package/sensor/dist/ui-server/constants.d.ts.map +1 -0
  573. package/sensor/dist/ui-server/constants.js +35 -0
  574. package/sensor/dist/ui-server/constants.js.map +1 -0
  575. package/sensor/dist/ui-server/highlight/index.d.ts +113 -0
  576. package/sensor/dist/ui-server/highlight/index.d.ts.map +1 -0
  577. package/sensor/dist/ui-server/highlight/index.js +307 -0
  578. package/sensor/dist/ui-server/highlight/index.js.map +1 -0
  579. package/sensor/dist/ui-server/highlight/languages.d.ts +36 -0
  580. package/sensor/dist/ui-server/highlight/languages.d.ts.map +1 -0
  581. package/sensor/dist/ui-server/highlight/languages.js +50 -0
  582. package/sensor/dist/ui-server/highlight/languages.js.map +1 -0
  583. package/sensor/dist/ui-server/index.d.ts +92 -0
  584. package/sensor/dist/ui-server/index.d.ts.map +1 -0
  585. package/sensor/dist/ui-server/index.js +399 -0
  586. package/sensor/dist/ui-server/index.js.map +1 -0
  587. package/sensor/dist/ui-server/open-browser.d.ts +31 -0
  588. package/sensor/dist/ui-server/open-browser.d.ts.map +1 -0
  589. package/sensor/dist/ui-server/open-browser.js +79 -0
  590. package/sensor/dist/ui-server/open-browser.js.map +1 -0
  591. package/sensor/dist/ui-server/security.d.ts +127 -0
  592. package/sensor/dist/ui-server/security.d.ts.map +1 -0
  593. package/sensor/dist/ui-server/security.js +332 -0
  594. package/sensor/dist/ui-server/security.js.map +1 -0
  595. package/sensor/dist/ui-server/static.d.ts +45 -0
  596. package/sensor/dist/ui-server/static.d.ts.map +1 -0
  597. package/sensor/dist/ui-server/static.js +165 -0
  598. package/sensor/dist/ui-server/static.js.map +1 -0
  599. package/sensor/dist/utils.d.ts +14 -2
  600. package/sensor/dist/utils.d.ts.map +1 -1
  601. package/sensor/dist/utils.js +25 -10
  602. package/sensor/dist/utils.js.map +1 -1
  603. package/src/sensor-adapter.mjs +5 -1
@@ -5,20 +5,26 @@
5
5
  * Prepared statements for CRUD operations on the knowledge graph.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.QueryBuilder = void 0;
8
+ exports.QueryBuilder = exports.DEPRIORITIZED_NAME_BONUS_SCALE = void 0;
9
9
  const utils_1 = require("../utils");
10
10
  const query_utils_1 = require("../search/query-utils");
11
11
  const query_parser_1 = require("../search/query-parser");
12
12
  const generated_detection_1 = require("../extraction/generated-detection");
13
13
  const identifier_segments_1 = require("../search/identifier-segments");
14
14
  /**
15
- * Path-only heuristic for files that should not be candidates for
16
- * "dominant file" detection: test/spec files and tool-generated files.
17
- * Generated files (`*.pb.go`, `*.pulsar.go`, mock outputs, …) often
18
- * have huge in-file edge counts that dwarf the real source — etcd's
19
- * `rpc.pb.go` has 4× the in-file edges of `server.go`.
15
+ * Files that should not be candidates for "dominant file" detection: test/spec
16
+ * files and tool-generated files. Generated files (`*.pb.go`, `*.pulsar.go`,
17
+ * mock outputs, …) often have huge in-file edge counts that dwarf the real
18
+ * source — etcd's `rpc.pb.go` has 4× the in-file edges of `server.go`.
19
+ *
20
+ * Path patterns plus, when the caller passes the indexed set, files whose
21
+ * HEADER declares them generated — a `payroll.go` full of generated CRUD has
22
+ * exactly the same edge-density problem as `rpc.pb.go` and nothing in its name
23
+ * to catch it (#1500).
20
24
  */
21
- function isLowValueFile(filePath) {
25
+ function isLowValueFile(filePath, generated) {
26
+ if (generated?.has(filePath))
27
+ return true;
22
28
  const lp = filePath.toLowerCase();
23
29
  return (/(?:^|\/)(tests?|__tests?__|spec)\//.test(lp) ||
24
30
  /_test\.go$/.test(lp) ||
@@ -34,6 +40,39 @@ function isLowValueFile(filePath) {
34
40
  (0, generated_detection_1.isGeneratedFile)(filePath));
35
41
  }
36
42
  const SQLITE_PARAM_CHUNK_SIZE = 500;
43
+ /**
44
+ * A SQL predicate: is the node aliased `alias` a member an INTERFACE declares?
45
+ *
46
+ * `method_signature` / `property_signature` enter the graph as `method` /
47
+ * `property` nodes hung off their interface by a `contains` edge (#1638). They
48
+ * have no body and originate no behaviour, so for a structural judgement about
49
+ * a FILE they are the interface restated, not an extra thing the file declares.
50
+ * See {@link QueryBuilder.getAmbientDeclarationPathsAmong}, the one caller, for
51
+ * why treating them as opaque would break that rule in three places at once.
52
+ *
53
+ * Seeks `idx_edges_target_kind`, so it costs a key lookup per row rather than a
54
+ * join over the whole edge table.
55
+ */
56
+ const IS_INTERFACE_MEMBER = (alias) => `EXISTS (
57
+ SELECT 1 FROM edges ce JOIN nodes owner ON owner.id = ce.source
58
+ WHERE ce.target = ${alias}.id AND ce.kind = 'contains' AND owner.kind = 'interface'
59
+ )`;
60
+ /**
61
+ * How much of the exact-name bonus a `deprioritize`d path keeps (#982). Damped
62
+ * rather than zeroed: a query that genuinely targets that tree must still rank
63
+ * it, the same "discount, don't erase" rule the path penalty follows.
64
+ *
65
+ * Derived rather than picked. `nameMatchBonus`'s prefix arm tops out below
66
+ * `10 + 30 = 40`, and a de-prioritized node also takes the -15 path penalty, so
67
+ * `80 * SCALE - 15 > 40` is what stops a damped WHOLE-QUERY exact match from
68
+ * losing to a mere prefix match. 0.75 clears it (45). Measured on a 62k-node
69
+ * django index: at 0.25 that invariant breaks in practice — `child`, `parent`
70
+ * and `method` lose rank 1 to `children`, `all_parents` and `method_decorator`
71
+ * — while crowd-out removal is almost flat between 0.75 and 0.5 (39 vs 40 of 88
72
+ * peripheral top-10 slots cleared), so a deeper discount buys little and costs
73
+ * the invariant. Pinned by a test.
74
+ */
75
+ exports.DEPRIORITIZED_NAME_BONUS_SCALE = 0.75;
37
76
  /**
38
77
  * `edges.resolved_by` strategy names that resolve a name WITHOUT verifying
39
78
  * that any import/require/include path actually connects the two files —
@@ -79,8 +118,11 @@ const UNCORROBORATED_FILTER_LANGUAGES_SQL_LIST = [...UNCORROBORATED_FILTER_LANGU
79
118
  * refs against newly-added node names.
80
119
  */
81
120
  function referenceNameTail(referenceName) {
82
- const idx = Math.max(referenceName.lastIndexOf('.'), referenceName.lastIndexOf(':'));
83
- return idx >= 0 ? referenceName.slice(idx + 1) : referenceName;
121
+ // Erlang refs carry a written arity (`f/1`, `mod::fn/2` — #1610); the tail a
122
+ // new symbol's plain name could match is the arity-less function name.
123
+ const base = referenceName.replace(/\/\d{1,3}$/, '') || referenceName;
124
+ const idx = Math.max(base.lastIndexOf('.'), base.lastIndexOf(':'));
125
+ return idx >= 0 ? base.slice(idx + 1) : base;
84
126
  }
85
127
  /**
86
128
  * Convert database row to Node object
@@ -164,6 +206,7 @@ function rowToFileRecord(row) {
164
206
  nodeCount: row.node_count,
165
207
  extractionVersion: row.extraction_version ?? 0,
166
208
  errors: row.errors ? (0, utils_1.safeJsonParse)(row.errors, undefined) : undefined,
209
+ generated: row.generated === 1,
167
210
  };
168
211
  }
169
212
  /**
@@ -176,9 +219,16 @@ class QueryBuilder {
176
219
  // whole project, not a symbol, so it carries no discriminative signal (#720).
177
220
  // Set once by the LatticeSensor instance; empty by default (no down-weighting).
178
221
  projectNameTokens = new Set();
222
+ isDeprioritizedPath;
223
+ // FTS5 availability flag — detected once at construction time (#1532)
224
+ _fts5Available;
179
225
  // Node cache for frequently accessed nodes (LRU-style, max 1000 entries)
180
226
  nodeCache = new Map();
181
227
  maxCacheSize = 1000;
228
+ // getDominantFile()'s answer, tagged with the database change stamp it was
229
+ // computed under (see getChangeStamp). Query-independent, so one value
230
+ // serves every explore until the database changes (#1864).
231
+ dominantFileMemo;
182
232
  // Prepared statements (lazily initialized)
183
233
  stmts = {};
184
234
  // Names whose segments were already written this session — skips re-splitting
@@ -195,6 +245,11 @@ class QueryBuilder {
195
245
  // (and therefore resolution's insertion-order disambiguation) is identical
196
246
  // to the one-row-per-run path.
197
247
  batchStmts = new Map();
248
+ // Kind-filtered edge reads build their SQL per call (a variable IN list),
249
+ // but from a handful of kind sets: prepare each shape once. Supertype walks
250
+ // and the member-lookup passes issue them per node, and preparing cost
251
+ // about a third of the read.
252
+ edgeKindStmts = new Map();
198
253
  static BATCH_SIZES = [128, 32, 8, 1];
199
254
  /**
200
255
  * Run `rows` through a multi-row `INSERT` built as `head + (tuple,)*n`,
@@ -230,6 +285,14 @@ class QueryBuilder {
230
285
  }
231
286
  constructor(db) {
232
287
  this.db = db;
288
+ // Detect FTS5 availability once (#1532)
289
+ try {
290
+ db.prepare("SELECT * FROM nodes_fts LIMIT 0").get();
291
+ this._fts5Available = true;
292
+ }
293
+ catch {
294
+ this._fts5Available = false;
295
+ }
233
296
  }
234
297
  /**
235
298
  * Swap the underlying connection in place. Used by pool workers'
@@ -247,6 +310,21 @@ class QueryBuilder {
247
310
  this.db = db;
248
311
  this.stmts = {};
249
312
  this.batchStmts.clear();
313
+ this.edgeKindStmts.clear();
314
+ // The change stamp is per connection, and fresh connections to two
315
+ // different databases report the same one — the memo goes with the old
316
+ // connection, or a worker following a rebuilt index keeps its answer (#1864).
317
+ this.dominantFileMemo = undefined;
318
+ }
319
+ edgeKindStmt(sql) {
320
+ let stmt = this.edgeKindStmts.get(sql);
321
+ if (!stmt) {
322
+ if (this.edgeKindStmts.size >= 64)
323
+ this.edgeKindStmts.delete(this.edgeKindStmts.keys().next().value);
324
+ stmt = this.db.prepare(sql);
325
+ this.edgeKindStmts.set(sql, stmt);
326
+ }
327
+ return stmt;
250
328
  }
251
329
  /** Set the normalized project-name tokens used to down-weight non-discriminative
252
330
  * query words in path scoring (#720). Called once when the project opens. */
@@ -257,6 +335,19 @@ class QueryBuilder {
257
335
  getProjectNameTokens() {
258
336
  return this.projectNameTokens;
259
337
  }
338
+ /**
339
+ * Set the predicate that marks a path as de-prioritized by the project's
340
+ * `lattice-sensor.json` `deprioritize` patterns (#982). Ranking-only: those paths
341
+ * stay indexed and findable, they just stop outranking first-party code.
342
+ * Called once when the project opens; undefined disables the lever.
343
+ */
344
+ setDeprioritizedPathMatcher(matcher) {
345
+ this.isDeprioritizedPath = matcher;
346
+ }
347
+ /** The `deprioritize` predicate (#982), so other rankers apply the same lever. */
348
+ getDeprioritizedPathMatcher() {
349
+ return this.isDeprioritizedPath;
350
+ }
260
351
  // ===========================================================================
261
352
  // Node Operations
262
353
  // ===========================================================================
@@ -718,10 +809,16 @@ class QueryBuilder {
718
809
  const uniqueIds = [...new Set(ids)];
719
810
  for (let i = 0; i < uniqueIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
720
811
  const chunk = uniqueIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
721
- const placeholders = chunk.map(() => '?').join(',');
722
- const rows = this.db
723
- .prepare(`SELECT id FROM nodes WHERE id IN (${placeholders})`)
724
- .all(...chunk);
812
+ // Every edge insert checks its endpoints here, a chunk at a time: the
813
+ // full-size statement is prepared once, the final partial chunk ad hoc.
814
+ let stmt;
815
+ if (chunk.length === SQLITE_PARAM_CHUNK_SIZE) {
816
+ stmt = this.stmts.existingNodeIdsFull ??= this.db.prepare(`SELECT id FROM nodes WHERE id IN (${new Array(SQLITE_PARAM_CHUNK_SIZE).fill('?').join(',')})`);
817
+ }
818
+ else {
819
+ stmt = this.db.prepare(`SELECT id FROM nodes WHERE id IN (${chunk.map(() => '?').join(',')})`);
820
+ }
821
+ const rows = stmt.all(...chunk);
725
822
  for (const row of rows) {
726
823
  out.add(row.id);
727
824
  }
@@ -747,16 +844,58 @@ class QueryBuilder {
747
844
  clearCache() {
748
845
  this.nodeCache.clear();
749
846
  }
847
+ /**
848
+ * Whether any node in `filePath` is exported — `getNodesByFile(f).some((n) =>
849
+ * n.isExported)` as one indexed probe, without decoding the file's nodes.
850
+ */
851
+ fileHasExportedNode(filePath) {
852
+ if (!this.stmts.fileHasExportedNode) {
853
+ this.stmts.fileHasExportedNode = this.db.prepare('SELECT 1 FROM nodes WHERE file_path = ? AND is_exported = 1 LIMIT 1');
854
+ }
855
+ return this.stmts.fileHasExportedNode.get(filePath) !== undefined;
856
+ }
857
+ /** The exported nodes of a file, in {@link getNodesByFile} order, decoding only those rows. */
858
+ getExportedNodesByFile(filePath) {
859
+ if (!this.stmts.getExportedNodesByFile) {
860
+ this.stmts.getExportedNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? AND is_exported = 1 ORDER BY start_line, id');
861
+ }
862
+ return this.stmts.getExportedNodesByFile.all(filePath).map(rowToNode);
863
+ }
864
+ /** The nodes of a file with one name, in {@link getNodesByFile} order, decoding only those rows. */
865
+ getNodesByFileAndName(filePath, name) {
866
+ if (!this.stmts.getNodesByFileAndName) {
867
+ this.stmts.getNodesByFileAndName = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? AND name = ? ORDER BY start_line, id');
868
+ }
869
+ return this.stmts.getNodesByFileAndName.all(filePath, name).map(rowToNode);
870
+ }
750
871
  /**
751
872
  * Get all nodes in a file
752
873
  */
753
874
  getNodesByFile(filePath) {
754
875
  if (!this.stmts.getNodesByFile) {
755
- this.stmts.getNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line');
876
+ this.stmts.getNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line, id');
756
877
  }
757
878
  const rows = this.stmts.getNodesByFile.all(filePath);
758
879
  return rows.map(rowToNode);
759
880
  }
881
+ /**
882
+ * Get all nodes in several files at once — one chunked `IN` query rather than
883
+ * one {@link getNodesByFile} per file (#1975).
884
+ */
885
+ getNodesByFiles(filePaths) {
886
+ const unique = [...new Set(filePaths)];
887
+ const out = [];
888
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
889
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
890
+ const placeholders = chunk.map(() => '?').join(',');
891
+ const rows = this.db
892
+ .prepare(`SELECT * FROM nodes WHERE file_path IN (${placeholders})`)
893
+ .all(...chunk);
894
+ for (const row of rows)
895
+ out.push(rowToNode(row));
896
+ }
897
+ return out;
898
+ }
760
899
  /**
761
900
  * Find the file that holds the densest concentration of the project's
762
901
  * internal call graph — the "core" file. Used by context-builder to
@@ -774,8 +913,46 @@ class QueryBuilder {
774
913
  * Excludes test/spec files from candidacy via path-pattern. The agent's
775
914
  * typical question is "how does X work", not "how is X tested", so
776
915
  * boosting a test file's directory would be a misfire.
916
+ *
917
+ * The answer depends only on the graph, never on the query, and the
918
+ * aggregation behind it scans every edge — seconds on a large index, paid
919
+ * by every generic explore (#1864). So it is memoized per database change
920
+ * stamp: recomputed only after something wrote to the database.
777
921
  */
778
922
  getDominantFile() {
923
+ // total_changes() counts writes that are later rolled back, so a result
924
+ // read inside a transaction could outlive a ROLLBACK under an unchanged
925
+ // stamp. Never keep one; `undefined` (a runtime without the getter) is
926
+ // treated the same way.
927
+ if (this.db.inTransaction !== false) {
928
+ this.dominantFileMemo = undefined;
929
+ return this.computeDominantFile();
930
+ }
931
+ const stamp = this.getChangeStamp();
932
+ if (this.dominantFileMemo?.stamp === stamp)
933
+ return this.dominantFileMemo.value;
934
+ const value = this.computeDominantFile();
935
+ this.dominantFileMemo = { stamp, value };
936
+ return value;
937
+ }
938
+ /**
939
+ * A value that differs whenever the database content may have changed
940
+ * since the last call, whoever changed it: `total_changes()` counts the
941
+ * rows this connection inserted, updated or deleted, and
942
+ * `PRAGMA data_version` moves when any OTHER connection — another process's
943
+ * sync, a CLI `lattice sensor index` beside a running MCP server — commits. Both
944
+ * are O(1), so no write path has to remember to invalidate anything.
945
+ * Coarse on purpose: any write, not just one to nodes/edges, forces a
946
+ * recompute, which only costs time, never a stale answer.
947
+ */
948
+ getChangeStamp() {
949
+ if (!this.stmts.getChangeStamp) {
950
+ this.stmts.getChangeStamp = this.db.prepare('SELECT total_changes() AS changes, (SELECT data_version FROM pragma_data_version) AS version');
951
+ }
952
+ const row = this.stmts.getChangeStamp.get();
953
+ return `${row.changes}:${row.version}`;
954
+ }
955
+ computeDominantFile() {
779
956
  if (!this.stmts.getDominantFile) {
780
957
  // Pull top 20 candidates; we then filter out test/generated files
781
958
  // in code (regex-grade matching that SQL LIKE can't express). The
@@ -796,7 +973,8 @@ class QueryBuilder {
796
973
  `);
797
974
  }
798
975
  const rows = this.stmts.getDominantFile.all();
799
- const filtered = rows.filter(r => !isLowValueFile(r.file_path));
976
+ const generated = this.getGeneratedPathsAmong(rows.map(r => r.file_path));
977
+ const filtered = rows.filter(r => !isLowValueFile(r.file_path, generated));
800
978
  if (filtered.length === 0 || filtered[0].edge_count < 20)
801
979
  return null;
802
980
  return {
@@ -829,7 +1007,8 @@ class QueryBuilder {
829
1007
  `);
830
1008
  }
831
1009
  const rows = this.stmts.getTopRouteFile.all();
832
- const filtered = rows.filter(r => !isLowValueFile(r.file_path));
1010
+ const generated = this.getGeneratedPathsAmong(rows.map(r => r.file_path));
1011
+ const filtered = rows.filter(r => !isLowValueFile(r.file_path, generated));
833
1012
  if (filtered.length === 0)
834
1013
  return null;
835
1014
  const totalRoutes = filtered.reduce((sum, r) => sum + r.cnt, 0);
@@ -859,6 +1038,9 @@ class QueryBuilder {
859
1038
  this.stmts.getRoutingManifest = this.db.prepare(`
860
1039
  SELECT
861
1040
  r.name AS url,
1041
+ r.id AS route_id,
1042
+ r.file_path AS route_file,
1043
+ r.start_line AS route_line,
862
1044
  h.name AS handler,
863
1045
  h.file_path AS handler_file,
864
1046
  h.start_line AS handler_line,
@@ -868,14 +1050,15 @@ class QueryBuilder {
868
1050
  JOIN nodes h ON e.target = h.id
869
1051
  WHERE r.kind = 'route'
870
1052
  AND e.kind IN ('references', 'calls')
871
- AND h.kind IN ('function', 'method', 'class')
1053
+ AND h.kind IN ('function', 'method', 'class', 'constant', 'variable')
872
1054
  ORDER BY r.file_path, r.start_line
873
1055
  LIMIT ?
874
1056
  `);
875
1057
  }
876
1058
  const rows = this.stmts.getRoutingManifest.all(limit);
877
1059
  // Drop test/generated handlers — same hygiene as elsewhere.
878
- const filtered = rows.filter(r => !isLowValueFile(r.handler_file));
1060
+ const generated = this.getGeneratedPathsAmong(rows.map(r => r.handler_file));
1061
+ const filtered = rows.filter(r => !isLowValueFile(r.handler_file, generated));
879
1062
  if (filtered.length < 3)
880
1063
  return null;
881
1064
  // Identify the file holding the most handlers (the "primary handler file").
@@ -898,6 +1081,9 @@ class QueryBuilder {
898
1081
  handlerFile: r.handler_file,
899
1082
  handlerLine: r.handler_line,
900
1083
  handlerKind: r.handler_kind,
1084
+ routeId: r.route_id,
1085
+ routeFile: r.route_file,
1086
+ routeLine: r.route_line,
901
1087
  })),
902
1088
  topHandlerFile,
903
1089
  topHandlerFileCount,
@@ -909,7 +1095,7 @@ class QueryBuilder {
909
1095
  */
910
1096
  getNodesByKind(kind) {
911
1097
  if (!this.stmts.getNodesByKind) {
912
- this.stmts.getNodesByKind = this.db.prepare('SELECT * FROM nodes WHERE kind = ?');
1098
+ this.stmts.getNodesByKind = this.db.prepare('SELECT * FROM nodes WHERE kind = ? ORDER BY file_path, start_line, id');
913
1099
  }
914
1100
  const rows = this.stmts.getNodesByKind.all(kind);
915
1101
  return rows.map(rowToNode);
@@ -924,11 +1110,29 @@ class QueryBuilder {
924
1110
  *iterateNodesByKind(kind) {
925
1111
  // Fresh statement per call (not a cached one): an iterator holds an open
926
1112
  // cursor, so a shared statement would conflict across overlapping scans.
927
- const stmt = this.db.prepare('SELECT * FROM nodes WHERE kind = ?');
1113
+ // Synthesis uses first/last-match precedence and caps: insertion order
1114
+ // changes on sync. idx_nodes_kind streams this canonical order without
1115
+ // materializing/sorting all of a large project's methods in memory.
1116
+ const stmt = this.db.prepare('SELECT * FROM nodes WHERE kind = ? ORDER BY file_path, start_line, id');
928
1117
  for (const row of stmt.iterate(kind)) {
929
1118
  yield rowToNode(row);
930
1119
  }
931
1120
  }
1121
+ /**
1122
+ * iterateNodesByKind narrowed to some languages, in the same canonical
1123
+ * order — the ORDER BY is total (`id` is unique), so this yields exactly the
1124
+ * nodes a caller filtering iterateNodesByKind by language would keep, in the
1125
+ * same sequence. A Go pass on a TypeScript monorepo otherwise materialized
1126
+ * every method in the project to find a couple of Go ones.
1127
+ */
1128
+ *iterateNodesByKindIn(kind, languages) {
1129
+ if (languages.length === 0)
1130
+ return;
1131
+ const stmt = this.db.prepare(`SELECT * FROM nodes WHERE kind = ? AND language IN (${languages.map(() => '?').join(', ')}) ORDER BY file_path, start_line, id`);
1132
+ for (const row of stmt.iterate(kind, ...languages)) {
1133
+ yield rowToNode(row);
1134
+ }
1135
+ }
932
1136
  /**
933
1137
  * Get all nodes in the database
934
1138
  */
@@ -948,7 +1152,7 @@ class QueryBuilder {
948
1152
  *iterateNodesByLanguageWithDecorator(language, decorator) {
949
1153
  // Fresh statement per call — an iterator holds an open cursor (see
950
1154
  // iterateNodesByKind).
951
- const stmt = this.db.prepare("SELECT * FROM nodes WHERE language = ? AND decorators LIKE '%' || ? || '%'");
1155
+ const stmt = this.db.prepare("SELECT * FROM nodes WHERE language = ? AND decorators LIKE '%' || ? || '%' ORDER BY file_path, start_line, id");
952
1156
  for (const row of stmt.iterate(language, `"${decorator}"`)) {
953
1157
  yield rowToNode(row);
954
1158
  }
@@ -964,11 +1168,26 @@ class QueryBuilder {
964
1168
  return new Set(rows.map((r) => r.language));
965
1169
  }
966
1170
  /**
967
- * Get nodes by exact name match (uses idx_nodes_name index)
1171
+ * Get nodes by exact name match (uses idx_nodes_name index).
1172
+ *
1173
+ * This is resolution's candidate list, and the ORDER BY is load-bearing for
1174
+ * index correctness, not cosmetic (CG-33). When a reference names a symbol
1175
+ * that several files define and nothing disambiguates them, resolution binds
1176
+ * to the first candidate — so without an ORDER BY the winner was decided by
1177
+ * rowid, i.e. by the order files happened to be WRITTEN. A full index writes
1178
+ * them in scan order; an incremental sync appends each file as it changes, so
1179
+ * the same tree resolved to different edges depending on how the index was
1180
+ * built, and a long-lived synced index drifted away from a rebuild of itself
1181
+ * (measured at 4.3% of distinct edges, mostly `calls`).
1182
+ *
1183
+ * `(file_path, start_line)` is a property of the CODE, so both paths now pick
1184
+ * the same candidate. The sort is paid once per distinct name per resolution
1185
+ * run — ReferenceResolver memoizes this in its nameCache — and the population
1186
+ * is capped by AMBIGUOUS_NAME_CEILING (#999).
968
1187
  */
969
1188
  getNodesByName(name) {
970
1189
  if (!this.stmts.getNodesByName) {
971
- this.stmts.getNodesByName = this.db.prepare('SELECT * FROM nodes WHERE name = ?');
1190
+ this.stmts.getNodesByName = this.db.prepare('SELECT * FROM nodes WHERE name = ? ORDER BY file_path, start_line, id');
972
1191
  }
973
1192
  const rows = this.stmts.getNodesByName.all(name);
974
1193
  return rows.map(rowToNode);
@@ -984,24 +1203,43 @@ class QueryBuilder {
984
1203
  const rows = this.stmts.getNodesByNamePrefix.all(prefix, prefix + '￿', limit);
985
1204
  return rows.map(rowToNode);
986
1205
  }
1206
+ /** File nodes whose basename starts with `prefix`, without a result cap. */
1207
+ getFileNodesByNamePrefix(prefix) {
1208
+ if (!this.stmts.getFileNodesByNamePrefix) {
1209
+ this.stmts.getFileNodesByNamePrefix = this.db.prepare("SELECT * FROM nodes WHERE kind = 'file' AND name >= ? AND name < ? ORDER BY name");
1210
+ }
1211
+ const rows = this.stmts.getFileNodesByNamePrefix.all(prefix, prefix + '￿');
1212
+ return rows.map(rowToNode);
1213
+ }
987
1214
  /**
988
1215
  * Get nodes by exact qualified name match (uses idx_nodes_qualified_name index)
989
1216
  */
990
1217
  getNodesByQualifiedNameExact(qualifiedName) {
991
1218
  if (!this.stmts.getNodesByQualifiedNameExact) {
992
- this.stmts.getNodesByQualifiedNameExact = this.db.prepare('SELECT * FROM nodes WHERE qualified_name = ?');
1219
+ this.stmts.getNodesByQualifiedNameExact = this.db.prepare('SELECT * FROM nodes WHERE qualified_name = ? ORDER BY file_path, start_line, id');
993
1220
  }
994
1221
  const rows = this.stmts.getNodesByQualifiedNameExact.all(qualifiedName);
995
1222
  return rows.map(rowToNode);
996
1223
  }
997
1224
  /**
998
- * Get nodes by lowercase name match (uses idx_nodes_lower_name expression index)
1225
+ * Get nodes by name, case-insensitively (seeks the idx_nodes_lower_name
1226
+ * expression index).
1227
+ *
1228
+ * The parameter is lowered in SQL rather than trusted to arrive lowered, so
1229
+ * the lookup means the same thing whatever casing a caller hands it. Written
1230
+ * as a bare `lower(name) = ?` it silently returned nothing for any input
1231
+ * carrying an uppercase letter, and — because SQLite's `lower()` folds ASCII
1232
+ * only while JavaScript's `.toLowerCase()` folds Unicode — a caller that
1233
+ * pre-lowered in JavaScript could not match a non-ASCII name at all.
1234
+ *
1235
+ * Note this hardens the query, not its one caller: `matchFuzzy` still lowers
1236
+ * in JavaScript before calling, so the non-ASCII gap remains open there.
999
1237
  */
1000
- getNodesByLowerName(lowerName) {
1238
+ getNodesByLowerName(name) {
1001
1239
  if (!this.stmts.getNodesByLowerName) {
1002
- this.stmts.getNodesByLowerName = this.db.prepare('SELECT * FROM nodes WHERE lower(name) = ?');
1240
+ this.stmts.getNodesByLowerName = this.db.prepare('SELECT * FROM nodes WHERE lower(name) = lower(?)');
1003
1241
  }
1004
- const rows = this.stmts.getNodesByLowerName.all(lowerName);
1242
+ const rows = this.stmts.getNodesByLowerName.all(name);
1005
1243
  return rows.map(rowToNode);
1006
1244
  }
1007
1245
  /**
@@ -1034,9 +1272,9 @@ class QueryBuilder {
1034
1272
  const text = parsed.text;
1035
1273
  const kinds = mergedKinds;
1036
1274
  const languages = mergedLanguages;
1037
- // First try FTS5 with prefix matching
1275
+ // First try FTS5 with prefix matching (skip if FTS5 not available, #1532)
1038
1276
  let results = text
1039
- ? this.searchNodesFTS(text, { kinds, languages, limit, offset })
1277
+ ? (this._fts5Available !== false ? this.searchNodesFTS(text, { kinds, languages, limit, offset }) : [])
1040
1278
  // Over-fetch by 5× when running filter-only (no text). The
1041
1279
  // post-scoring path: + name: filters can be very selective, so
1042
1280
  // a smaller multiplier risks returning fewer than `limit`
@@ -1059,12 +1297,25 @@ class QueryBuilder {
1059
1297
  // pushing them past the FTS fetch limit before post-hoc scoring can help.
1060
1298
  // Use the max BM25 score as the base so the nameMatchBonus (exact=30 vs
1061
1299
  // prefix=20) actually differentiates them after rescoring.
1300
+ //
1301
+ // Whole-name equality MUST be written as `lower(name) = lower(?)` so it
1302
+ // seeks `idx_nodes_lower_name`. The equivalent `name = ? COLLATE NOCASE`
1303
+ // matches no index — `idx_nodes_name` is BINARY-collated and the expression
1304
+ // index only matches the same expression — and degrades to a full table
1305
+ // scan. The `LIMIT 20` does not rescue it: SQLite can only stop early once
1306
+ // it has produced 20 rows, and this runs once per query term, most of which
1307
+ // name nothing in the corpus. Measured per term on an unmatched term:
1308
+ // 0.08ms on gin (2.5k nodes), 0.39ms on excalidraw (11k), 2.4ms on django
1309
+ // (62k) — and growing with the corpus, where the seek is flat at ~0.002ms.
1310
+ // Lowering the parameter in SQL rather than in JS is deliberate: SQLite's
1311
+ // `lower()` and NOCASE both fold ASCII only, while JS `.toLowerCase()`
1312
+ // folds Unicode, which would silently stop matching non-ASCII names.
1062
1313
  if (results.length > 0 && query) {
1063
1314
  const existingIds = new Set(results.map(r => r.node.id));
1064
1315
  const maxFtsScore = Math.max(...results.map(r => r.score));
1065
1316
  const terms = query.split(/\s+/).filter(t => t.length >= 2);
1066
1317
  for (const term of terms) {
1067
- let sql = 'SELECT * FROM nodes WHERE name = ? COLLATE NOCASE';
1318
+ let sql = 'SELECT * FROM nodes WHERE lower(name) = lower(?)';
1068
1319
  const params = [term];
1069
1320
  if (kinds && kinds.length > 0) {
1070
1321
  sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
@@ -1087,13 +1338,24 @@ class QueryBuilder {
1087
1338
  // Apply multi-signal scoring
1088
1339
  if (results.length > 0 && (text || query)) {
1089
1340
  const scoringQuery = text || query;
1090
- results = results.map(r => ({
1091
- ...r,
1092
- score: r.score
1093
- + (0, query_utils_1.kindBonus)(r.node.kind)
1094
- + (0, query_utils_1.scorePathRelevance)(r.node.filePath, scoringQuery, this.projectNameTokens)
1095
- + (0, query_utils_1.nameMatchBonus)(r.node.name, scoringQuery),
1096
- }));
1341
+ results = results.map(r => {
1342
+ // A path the project de-prioritized is saying its symbol NAMES are not
1343
+ // the answer, so the exact-name bonus has to be damped too. The -15 path
1344
+ // penalty alone cannot do it: the bonus is additive and larger (measured
1345
+ // on #982's repro, a `usage()` helper sat at 74.8 vs 51.2 for the top
1346
+ // product symbol — -15 lands at 59.8, still ahead). Damped, not zeroed,
1347
+ // so the tree stays findable when it genuinely is what you asked for.
1348
+ // Evaluated once and reused: the predicate stats the config file.
1349
+ const deprioritized = this.isDeprioritizedPath?.(r.node.filePath) ?? false;
1350
+ const nameBonus = (0, query_utils_1.nameMatchBonus)(r.node.name, scoringQuery);
1351
+ return {
1352
+ ...r,
1353
+ score: r.score
1354
+ + (0, query_utils_1.kindBonus)(r.node.kind)
1355
+ + (0, query_utils_1.scorePathRelevance)(r.node.filePath, scoringQuery, this.projectNameTokens, deprioritized)
1356
+ + (deprioritized ? Math.round(nameBonus * exports.DEPRIORITIZED_NAME_BONUS_SCALE) : nameBonus),
1357
+ };
1358
+ });
1097
1359
  results.sort((a, b) => b.score - a.score);
1098
1360
  // Trim to requested limit after rescoring
1099
1361
  if (results.length > limit) {
@@ -1204,6 +1466,46 @@ class QueryBuilder {
1204
1466
  }
1205
1467
  return results;
1206
1468
  }
1469
+ /** Bounded miss diagnostics, independent of relevance filters and ranking. */
1470
+ getExploreMissDiagnostics(query) {
1471
+ const words = [...new Set((query.match(/[\p{L}\p{N}]+/gu) ?? []).map(w => w.toLowerCase()))];
1472
+ const checked = words.filter(w => w.length <= 64).slice(0, 16);
1473
+ const limited = checked.length !== words.length;
1474
+ if (checked.length === 0)
1475
+ return { matched: [], unmatched: [], candidates: [], limited };
1476
+ // EXISTS uses FTS postings and the segment primary key, never source scans.
1477
+ // Vocab rows can outlive deleted definitions, so verify them against nodes.
1478
+ const rows = this.db.prepare(`
1479
+ WITH words(word, pattern) AS (VALUES ${checked.map(() => '(?, ?)').join(', ')})
1480
+ SELECT word, (
1481
+ EXISTS (SELECT 1 FROM nodes_fts WHERE nodes_fts MATCH pattern)
1482
+ OR EXISTS (
1483
+ SELECT 1 FROM name_segment_vocab v WHERE v.segment = word
1484
+ AND EXISTS (SELECT 1 FROM nodes n WHERE n.name = v.name AND n.kind NOT IN ('file', 'import'))
1485
+ )
1486
+ ) AS matched FROM words
1487
+ `).all(...checked.flatMap(w => [w, `{name qualified_name signature docstring} : "${w}"*`]));
1488
+ const names = this.db.prepare(`
1489
+ SELECT name FROM (
1490
+ SELECT v.name FROM name_segment_vocab v
1491
+ WHERE v.segment IN (${checked.map(() => '?').join(', ')})
1492
+ AND EXISTS (SELECT 1 FROM nodes n WHERE n.name = v.name AND n.kind NOT IN ('file', 'import'))
1493
+ LIMIT 12
1494
+ )
1495
+ UNION ALL
1496
+ SELECT name FROM (
1497
+ SELECT n.name FROM nodes_fts JOIN nodes n ON n.rowid = nodes_fts.rowid
1498
+ WHERE nodes_fts MATCH ? AND n.kind NOT IN ('file', 'import')
1499
+ LIMIT 12
1500
+ )
1501
+ `).all(...checked, `name : (${checked.map(w => `"${w}"*`).join(' OR ')})`);
1502
+ return {
1503
+ matched: rows.filter(r => r.matched).map(r => r.word),
1504
+ unmatched: rows.filter(r => !r.matched).map(r => r.word),
1505
+ candidates: [...new Set(names.map(r => r.name))].slice(0, 12),
1506
+ limited,
1507
+ };
1508
+ }
1207
1509
  /**
1208
1510
  * FTS5 search with prefix matching
1209
1511
  */
@@ -1332,9 +1634,16 @@ class QueryBuilder {
1332
1634
  // Pass 1: Find which files contain distinctive (rare) symbols from the query.
1333
1635
  // Pass 2: Query each name, boosting results that co-locate with distinctive symbols.
1334
1636
  // Pass 1: Find files containing each queried name, identify distinctive names
1637
+ //
1638
+ // Both passes spell whole-name equality as `lower(name) = lower(?)` so they
1639
+ // seek `idx_nodes_lower_name` — see the note in `searchNodes` for why the
1640
+ // `name = ? COLLATE NOCASE` form full-scans instead. This path is the one
1641
+ // that hurts most: it runs both passes for every symbol extracted from the
1642
+ // query, and extraction is generous, so most of those names are absent from
1643
+ // the corpus and never reach either LIMIT.
1335
1644
  const nameToFiles = new Map();
1336
1645
  for (const name of names) {
1337
- let sql = 'SELECT DISTINCT file_path FROM nodes WHERE name COLLATE NOCASE = ?';
1646
+ let sql = 'SELECT DISTINCT file_path FROM nodes WHERE lower(name) = lower(?)';
1338
1647
  const params = [name];
1339
1648
  if (kinds && kinds.length > 0) {
1340
1649
  sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
@@ -1360,7 +1669,7 @@ class QueryBuilder {
1360
1669
  let sql = `
1361
1670
  SELECT nodes.*, 1.0 as score
1362
1671
  FROM nodes
1363
- WHERE name COLLATE NOCASE = ?
1672
+ WHERE lower(name) = lower(?)
1364
1673
  `;
1365
1674
  const params = [name];
1366
1675
  if (kinds && kinds.length > 0) {
@@ -1434,6 +1743,30 @@ class QueryBuilder {
1434
1743
  // ===========================================================================
1435
1744
  // Edge Operations
1436
1745
  // ===========================================================================
1746
+ /** Must run before file replacement/deletion cascades the endpoint edges. */
1747
+ hasSynthesizedEdgesTouchingFile(filePath) {
1748
+ const owned = "CASE WHEN json_valid(e.metadata) THEN json_extract(e.metadata, '$.synthesizedBy') END IS NOT NULL";
1749
+ for (const endpoint of ['source', 'target']) {
1750
+ if (this.db.prepare(`SELECT 1 FROM nodes n JOIN edges e ON e.${endpoint} = n.id
1751
+ WHERE n.file_path = ? AND ${owned} LIMIT 1`).get(filePath))
1752
+ return true;
1753
+ }
1754
+ // Wiring often lives in a third file, with neither endpoint in it.
1755
+ return !!this.db.prepare(`SELECT 1 FROM edges e WHERE ${owned}
1756
+ AND CASE WHEN json_valid(e.metadata) THEN json_extract(e.metadata, '$.registeredAt') END >= ?
1757
+ AND CASE WHEN json_valid(e.metadata) THEN json_extract(e.metadata, '$.registeredAt') END < ? LIMIT 1`).get(`${filePath}:`, `${filePath};`);
1758
+ }
1759
+ wasSynthesisInput(filePath) {
1760
+ return !!this.db.prepare('SELECT 1 FROM synthesis_inputs WHERE file_path = ?').get(filePath);
1761
+ }
1762
+ replaceSynthesisInputs(files) {
1763
+ this.db.transaction(() => {
1764
+ this.db.exec('DELETE FROM synthesis_inputs');
1765
+ const insert = this.db.prepare('INSERT INTO synthesis_inputs(file_path) VALUES (?)');
1766
+ for (const file of files)
1767
+ insert.run(file);
1768
+ })();
1769
+ }
1437
1770
  /**
1438
1771
  * Insert a new edge
1439
1772
  */
@@ -1499,7 +1832,8 @@ class QueryBuilder {
1499
1832
  this.stmts.deleteEdgesBySource.run(sourceId);
1500
1833
  }
1501
1834
  /**
1502
- * Get outgoing edges from a node
1835
+ * Get outgoing edges from a node. Preserve the source/kind index order
1836
+ * (calls before imports/references), then break ties deterministically.
1503
1837
  */
1504
1838
  getOutgoingEdges(sourceId, kinds, provenance) {
1505
1839
  if ((kinds && kinds.length > 0) || provenance) {
@@ -1513,30 +1847,649 @@ class QueryBuilder {
1513
1847
  sql += ' AND provenance = ?';
1514
1848
  params.push(provenance);
1515
1849
  }
1516
- const rows = this.db.prepare(sql).all(...params);
1850
+ sql += ' ORDER BY kind, target, line, col';
1851
+ const rows = this.edgeKindStmt(sql).all(...params);
1517
1852
  return rows.map(rowToEdge);
1518
1853
  }
1519
1854
  if (!this.stmts.getEdgesBySource) {
1520
- this.stmts.getEdgesBySource = this.db.prepare('SELECT * FROM edges WHERE source = ?');
1855
+ this.stmts.getEdgesBySource = this.db.prepare('SELECT * FROM edges WHERE source = ? ORDER BY kind, target, line, col');
1521
1856
  }
1522
1857
  const rows = this.stmts.getEdgesBySource.all(sourceId);
1523
1858
  return rows.map(rowToEdge);
1524
1859
  }
1525
1860
  /**
1526
- * Get incoming edges to a node
1861
+ * Get incoming edges to a node. Kind must precede opaque source IDs:
1862
+ * file IDs sort before function IDs, so source-first ordering lets imports
1863
+ * displace actual calls in capped caller lists. Keep deterministic ties
1864
+ * without changing the target/kind index's established kind precedence.
1527
1865
  */
1528
1866
  getIncomingEdges(targetId, kinds) {
1529
1867
  if (kinds && kinds.length > 0) {
1530
- const sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')})`;
1531
- const rows = this.db.prepare(sql).all(targetId, ...kinds);
1868
+ const sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')}) ORDER BY kind, source, line, col`;
1869
+ const rows = this.edgeKindStmt(sql).all(targetId, ...kinds);
1532
1870
  return rows.map(rowToEdge);
1533
1871
  }
1534
1872
  if (!this.stmts.getEdgesByTarget) {
1535
- this.stmts.getEdgesByTarget = this.db.prepare('SELECT * FROM edges WHERE target = ?');
1873
+ this.stmts.getEdgesByTarget = this.db.prepare('SELECT * FROM edges WHERE target = ? ORDER BY kind, source, line, col');
1536
1874
  }
1537
1875
  const rows = this.stmts.getEdgesByTarget.all(targetId);
1538
1876
  return rows.map(rowToEdge);
1539
1877
  }
1878
+ /**
1879
+ * Outgoing edges for MANY source nodes in one query.
1880
+ *
1881
+ * The batch form of {@link getOutgoingEdges}. Building a nested outline needs
1882
+ * the `contains` edges of every container in a file at once; doing that one
1883
+ * source at a time is a query per symbol on files that have hundreds.
1884
+ */
1885
+ getOutgoingEdgesFrom(sourceIds, kinds) {
1886
+ if (sourceIds.length === 0)
1887
+ return [];
1888
+ const unique = [...new Set(sourceIds)];
1889
+ const out = [];
1890
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1891
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1892
+ const placeholders = chunk.map(() => '?').join(',');
1893
+ let sql = `SELECT * FROM edges WHERE source IN (${placeholders})`;
1894
+ const params = [...chunk];
1895
+ if (kinds && kinds.length > 0) {
1896
+ sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
1897
+ params.push(...kinds);
1898
+ }
1899
+ const rows = this.db.prepare(sql).all(...params);
1900
+ for (const row of rows)
1901
+ out.push(rowToEdge(row));
1902
+ }
1903
+ return out;
1904
+ }
1905
+ /**
1906
+ * Fan-in (total incoming edge count) for MANY nodes in one query.
1907
+ *
1908
+ * The per-node alternative — `getIncomingEdges(id).length` — is an indexed
1909
+ * lookup each, but a symbol screen rendering a couple of hundred callees
1910
+ * would issue a couple of hundred of them. Ids with no incoming edges are
1911
+ * absent from the map rather than present as 0, so callers can tell "no
1912
+ * edges" from "not asked about".
1913
+ */
1914
+ countIncomingEdges(ids) {
1915
+ const out = new Map();
1916
+ if (ids.length === 0)
1917
+ return out;
1918
+ const unique = [...new Set(ids)];
1919
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1920
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1921
+ const placeholders = chunk.map(() => '?').join(',');
1922
+ const rows = this.db
1923
+ .prepare(
1924
+ // A test's request onto a route (tier-synthesizer's `test-request`)
1925
+ // is not a production caller: forty tests hitting one endpoint must
1926
+ // not make it a hub the Steps walk refuses to enter.
1927
+ `SELECT target, COUNT(*) AS count FROM edges WHERE target IN (${placeholders})
1928
+ AND (metadata IS NULL OR metadata NOT LIKE '%"synthesizedBy":"test-request"%') GROUP BY target`)
1929
+ .all(...chunk);
1930
+ for (const row of rows)
1931
+ out.set(row.target, row.count);
1932
+ }
1933
+ return out;
1934
+ }
1935
+ /**
1936
+ * Incoming edges for MANY target nodes in one query — the mirror of
1937
+ * {@link getOutgoingEdgesFrom}. Needed wherever a whole file's inbound edges
1938
+ * are wanted at once ("which files import anything in this one?").
1939
+ */
1940
+ getIncomingEdgesTo(targetIds, kinds) {
1941
+ if (targetIds.length === 0)
1942
+ return [];
1943
+ const unique = [...new Set(targetIds)];
1944
+ const out = [];
1945
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1946
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1947
+ const placeholders = chunk.map(() => '?').join(',');
1948
+ let sql = `SELECT * FROM edges WHERE target IN (${placeholders})`;
1949
+ const params = [...chunk];
1950
+ if (kinds && kinds.length > 0) {
1951
+ sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
1952
+ params.push(...kinds);
1953
+ }
1954
+ const rows = this.db.prepare(sql).all(...params);
1955
+ for (const row of rows)
1956
+ out.push(rowToEdge(row));
1957
+ }
1958
+ return out;
1959
+ }
1960
+ /**
1961
+ * Fan-out (total outgoing edge count) for MANY nodes in one query — the
1962
+ * mirror of {@link countIncomingEdges}. Ids with no outgoing edges are absent
1963
+ * from the map rather than present as 0.
1964
+ */
1965
+ countOutgoingEdges(ids) {
1966
+ const out = new Map();
1967
+ if (ids.length === 0)
1968
+ return out;
1969
+ const unique = [...new Set(ids)];
1970
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
1971
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
1972
+ const placeholders = chunk.map(() => '?').join(',');
1973
+ const rows = this.db
1974
+ .prepare(`SELECT source, COUNT(*) AS count FROM edges WHERE source IN (${placeholders}) GROUP BY source`)
1975
+ .all(...chunk);
1976
+ for (const row of rows)
1977
+ out.set(row.source, row.count);
1978
+ }
1979
+ return out;
1980
+ }
1981
+ /**
1982
+ * Symbols nothing in the index points at — the candidate set behind the dead
1983
+ * code list (`src/graph/dead-code.ts`).
1984
+ *
1985
+ * "Points at" is every edge kind EXCEPT `contains`: a class containing a
1986
+ * method is structure, not use, and counting it would make every member look
1987
+ * reached by its own container. A self-edge is excluded for the same reason
1988
+ * a recursive function is not its own caller.
1989
+ *
1990
+ * One scan, one index probe per candidate. `NOT EXISTS` over
1991
+ * `idx_edges_target_kind` is what keeps it that way — the alternative
1992
+ * (`LEFT JOIN edges … GROUP BY`) builds a row per edge for the whole table
1993
+ * before discarding all but the empty groups. Ordered by position so the
1994
+ * answer is stable across runs and groups by file without a second sort.
1995
+ *
1996
+ * The result is deliberately NOT called dead code: an unreferenced symbol is
1997
+ * a symbol with no STATIC reference, and the caller applies the exclusions
1998
+ * (tests, generated files, overrides, unresolved names) that turn the
1999
+ * candidate set into a claim worth making.
2000
+ */
2001
+ getUnreferencedNodes(kinds, limit) {
2002
+ if (kinds.length === 0 || limit <= 0)
2003
+ return [];
2004
+ const placeholders = kinds.map(() => '?').join(',');
2005
+ const rows = this.db
2006
+ .prepare(`SELECT n.*, COALESCE(f.generated, 0) AS file_generated
2007
+ FROM nodes n
2008
+ LEFT JOIN files f ON f.path = n.file_path
2009
+ WHERE n.kind IN (${placeholders})
2010
+ AND NOT EXISTS (
2011
+ SELECT 1 FROM edges e
2012
+ WHERE e.target = n.id
2013
+ AND e.kind != 'contains'
2014
+ AND e.source != n.id
2015
+ )
2016
+ ORDER BY n.file_path, n.start_line, n.name
2017
+ LIMIT ?`)
2018
+ .all(...kinds, limit);
2019
+ return rows.map((row) => ({ node: rowToNode(row), generated: row.file_generated === 1 }));
2020
+ }
2021
+ /**
2022
+ * Which of `names` the index holds an UNRESOLVED reference to.
2023
+ *
2024
+ * The point is honesty about our own blind spots. A `failed` row in
2025
+ * `unresolved_refs` records that some file referenced a name and the resolver
2026
+ * could not decide what it meant — so a symbol with that name cannot be
2027
+ * called unreferenced, whatever the edge table says. It is deliberately
2028
+ * matched loosely, on the reference name AND on its tail (`util.greet` →
2029
+ * `greet`), because the question being asked is "could this name be the one
2030
+ * we failed to follow", and a maybe has to count as a yes.
2031
+ *
2032
+ * Bounded-lookup like {@link getGeneratedPathsAmong}: the caller holds a
2033
+ * candidate list, so this is a chunked probe over `idx_unresolved_name`, not
2034
+ * a scan of the table.
2035
+ */
2036
+ getUnresolvedNamesAmong(names) {
2037
+ const unique = [...new Set(names)].filter((name) => name.length > 0);
2038
+ const found = new Set();
2039
+ if (unique.length === 0)
2040
+ return found;
2041
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
2042
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
2043
+ const placeholders = chunk.map(() => '?').join(',');
2044
+ const rows = this.db
2045
+ .prepare(`SELECT DISTINCT reference_name AS name FROM unresolved_refs
2046
+ WHERE reference_name IN (${placeholders})
2047
+ UNION
2048
+ SELECT DISTINCT name_tail AS name FROM unresolved_refs
2049
+ WHERE name_tail IN (${placeholders})`)
2050
+ .all(...chunk, ...chunk);
2051
+ for (const row of rows)
2052
+ found.add(row.name);
2053
+ }
2054
+ return found;
2055
+ }
2056
+ /**
2057
+ * Which of `nodeIds` extend or implement a type the resolver could not follow.
2058
+ * An ancestor outside the index (`React.Component`, `stream.Transform`, a
2059
+ * framework interface) leaves no edge, only this `extends` / `implements`
2060
+ * row, so it is the one record that such an ancestor exists (#1973).
2061
+ * Chunked probe over `idx_unresolved_from_node`.
2062
+ */
2063
+ getUnresolvedSupertypeSourcesAmong(nodeIds) {
2064
+ const unique = [...new Set(nodeIds)];
2065
+ const found = new Set();
2066
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
2067
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
2068
+ const placeholders = chunk.map(() => '?').join(',');
2069
+ const rows = this.db
2070
+ .prepare(`SELECT DISTINCT from_node_id AS id FROM unresolved_refs
2071
+ WHERE from_node_id IN (${placeholders})
2072
+ AND reference_kind IN ('extends', 'implements')`)
2073
+ .all(...chunk);
2074
+ for (const row of rows)
2075
+ found.add(row.id);
2076
+ }
2077
+ return found;
2078
+ }
2079
+ /**
2080
+ * Which of `names` are carried by MORE THAN ONE symbol, at least one of which
2081
+ * something points at.
2082
+ *
2083
+ * The false positive this exists to kill: `LatticeSensor.getTopRouteFile` calls
2084
+ * `this.queries.getTopRouteFile()`, and the resolver — which prefers a
2085
+ * same-name definition in the call site's own file — attaches that edge to
2086
+ * the *calling* method. One of the two ends up with a self-edge and the other
2087
+ * with nothing at all, and neither is unreferenced. From the edge table the
2088
+ * mis-resolution and a genuinely unused twin are the same picture, so the
2089
+ * claim is not made about either.
2090
+ *
2091
+ * Both halves of the condition are load-bearing. **More than one symbol**:
2092
+ * a uniquely-named function that only calls itself is genuinely dead, and
2093
+ * excluding every recursive function would gut the list. **Self-edges
2094
+ * counted**: the self-edge IS the fingerprint of the mis-resolution above, so
2095
+ * it has to count as evidence that this name resolves somewhere.
2096
+ *
2097
+ * Chunked probe over `idx_nodes_name`, bounded by the caller's candidate list.
2098
+ */
2099
+ getAmbiguousReferencedNames(names) {
2100
+ const unique = [...new Set(names)].filter((name) => name.length > 0);
2101
+ const found = new Set();
2102
+ if (unique.length === 0)
2103
+ return found;
2104
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
2105
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
2106
+ const placeholders = chunk.map(() => '?').join(',');
2107
+ const rows = this.db
2108
+ .prepare(`SELECT name FROM (
2109
+ SELECT n.name AS name,
2110
+ EXISTS (
2111
+ SELECT 1 FROM edges e
2112
+ WHERE e.target = n.id AND e.kind != 'contains'
2113
+ ) AS referenced
2114
+ FROM nodes n
2115
+ WHERE n.name IN (${placeholders})
2116
+ )
2117
+ GROUP BY name
2118
+ HAVING COUNT(*) > 1 AND SUM(referenced) > 0`)
2119
+ .all(...chunk);
2120
+ for (const row of rows)
2121
+ found.add(row.name);
2122
+ }
2123
+ return found;
2124
+ }
2125
+ /**
2126
+ * Which of the given languages the index records an EXPORT marker for.
2127
+ *
2128
+ * A self-measurement, and the honest basis for a whole class of exclusion.
2129
+ * The dead code report's strongest filter is "exported symbols may be reached
2130
+ * from outside this repository" — and that filter silently does nothing for a
2131
+ * language whose exports are not recorded, either because the extractor does
2132
+ * not record them (Rust `pub`) or because the language has no such concept at
2133
+ * all (Python, C, Ruby: the header or the module IS the surface). Rather than
2134
+ * carry a table of which is which, ask the index: if nothing in this language
2135
+ * is marked exported, the filter did not run, and no claim about outside
2136
+ * reachability can be made for it.
2137
+ *
2138
+ * `idx_nodes_language` covers the grouping; the caller passes the handful of
2139
+ * languages its candidates are actually in.
2140
+ */
2141
+ getLanguagesWithExports(languages) {
2142
+ const unique = [...new Set(languages)].filter((language) => language.length > 0);
2143
+ const found = new Set();
2144
+ if (unique.length === 0)
2145
+ return found;
2146
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
2147
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
2148
+ const placeholders = chunk.map(() => '?').join(',');
2149
+ const rows = this.db
2150
+ .prepare(`SELECT language, MAX(is_exported) AS any_exported
2151
+ FROM nodes
2152
+ WHERE language IN (${placeholders})
2153
+ GROUP BY language`)
2154
+ .all(...chunk);
2155
+ for (const row of rows)
2156
+ if (row.any_exported === 1)
2157
+ found.add(row.language);
2158
+ }
2159
+ return found;
2160
+ }
2161
+ /**
2162
+ * The nodes with the most DISTINCT dependents, most first.
2163
+ *
2164
+ * "Distinct" is the difference that matters: a helper called forty times from
2165
+ * one function has a fan-in of 40 but exactly one dependent. This counts the
2166
+ * second thing — the number a reader means by "N callers" — so the top of
2167
+ * this list is the set of symbols a change actually radiates furthest from.
2168
+ *
2169
+ * `contains` is excluded because it is structure, not dependency: counting it
2170
+ * would rank every file and class above the code they hold.
2171
+ */
2172
+ getTopDependedOn(limit) {
2173
+ if (limit <= 0)
2174
+ return [];
2175
+ const rows = this.db
2176
+ .prepare(`SELECT target AS nodeId, COUNT(DISTINCT source) AS dependents
2177
+ FROM edges
2178
+ WHERE kind != 'contains' AND source != target
2179
+ GROUP BY target
2180
+ ORDER BY dependents DESC
2181
+ LIMIT ?`)
2182
+ .all(limit);
2183
+ return rows;
2184
+ }
2185
+ /**
2186
+ * The graph's executable roots — files that RUN something at module level,
2187
+ * ranked by how much of the project they set in motion.
2188
+ *
2189
+ * The engine records a statement at the top level of a file as an edge from
2190
+ * the *file* node, so `src/bin/lattice-sensor.ts` calling `program.parse()` at
2191
+ * module scope is a `calls` edge out of a `file`. That set is what makes the
2192
+ * roots of a dependency graph visible: a library module holds definitions and
2193
+ * runs nothing until someone imports it, while a CLI, a worker entry or a
2194
+ * build script does its work on the way down the file. `instantiates` counts
2195
+ * the same way — `new Server(...)` at module scope is the same act.
2196
+ *
2197
+ * A call made while initializing a module-level `variable` / `constant` —
2198
+ * `const service = new Service()`, `app = FastAPI()` — is attributed to the
2199
+ * declared name (#693), not to the file, so the file's own edges alone would
2200
+ * miss most of what a real entry point runs. Those names are the file's
2201
+ * top-level code too, so `tops` counts them alongside the file node.
2202
+ *
2203
+ * Ranking multiplies the two things an entry point does: it runs (calls), and
2204
+ * it wires the project together (distinct other files its symbols reach). One
2205
+ * alone is misleading — a registration table makes hundreds of module-level
2206
+ * calls into itself, and a barrel file imports everything and runs nothing.
2207
+ * The product puts the file that does both at the top.
2208
+ */
2209
+ getTopCallingFiles(limit) {
2210
+ if (limit <= 0)
2211
+ return [];
2212
+ return this.db
2213
+ .prepare(`WITH tops AS (
2214
+ SELECT n.id AS file_id, n.id AS src
2215
+ FROM nodes n
2216
+ WHERE n.kind = 'file'
2217
+ UNION ALL
2218
+ SELECT c.source AS file_id, c.target AS src
2219
+ FROM edges c
2220
+ JOIN nodes f ON f.id = c.source
2221
+ JOIN nodes v ON v.id = c.target
2222
+ WHERE c.kind = 'contains'
2223
+ AND f.kind = 'file'
2224
+ AND v.kind IN ('variable', 'constant')
2225
+ ),
2226
+ runs AS (
2227
+ SELECT t.file_id AS id, COUNT(*) AS calls
2228
+ FROM tops t
2229
+ JOIN edges e ON e.source = t.src
2230
+ WHERE e.kind IN ('calls', 'instantiates')
2231
+ GROUP BY t.file_id
2232
+ ),
2233
+ cand AS (
2234
+ SELECT r.id AS id, n.file_path AS fp, r.calls AS calls
2235
+ FROM runs r JOIN nodes n ON n.id = r.id
2236
+ ),
2237
+ wires AS (
2238
+ SELECT sn.file_path AS fp, COUNT(DISTINCT tn.file_path) AS reaches
2239
+ FROM edges e
2240
+ JOIN nodes sn ON sn.id = e.source
2241
+ JOIN nodes tn ON tn.id = e.target
2242
+ WHERE e.kind != 'contains'
2243
+ AND sn.file_path <> tn.file_path
2244
+ AND sn.file_path IN (SELECT fp FROM cand)
2245
+ GROUP BY sn.file_path
2246
+ )
2247
+ SELECT c.id AS nodeId,
2248
+ c.fp AS filePath,
2249
+ c.calls AS calls,
2250
+ COALESCE(w.reaches, 0) AS reaches,
2251
+ c.calls * (1 + COALESCE(w.reaches, 0)) AS score
2252
+ FROM cand c LEFT JOIN wires w ON w.fp = c.fp
2253
+ ORDER BY score DESC, calls DESC, filePath
2254
+ LIMIT ?`)
2255
+ .all(limit);
2256
+ }
2257
+ /**
2258
+ * How many OTHER files depend on each of the given files.
2259
+ *
2260
+ * Counted through the symbols, not the file nodes: an `imports` edge points
2261
+ * at the imported symbol, so a file node almost never receives one and
2262
+ * counting edges into it would report every file as depended on by nobody.
2263
+ * Same-file edges are excluded, which is what makes zero mean "nothing else
2264
+ * in the index reaches into this file" — the honest reading of a root.
2265
+ */
2266
+ getFileDependentCounts(filePaths) {
2267
+ if (filePaths.length === 0)
2268
+ return [];
2269
+ return this.db
2270
+ .prepare(`SELECT tn.file_path AS filePath, COUNT(DISTINCT sn.file_path) AS dependents
2271
+ FROM edges e
2272
+ JOIN nodes tn ON tn.id = e.target
2273
+ JOIN nodes sn ON sn.id = e.source
2274
+ WHERE e.kind != 'contains'
2275
+ AND tn.file_path IN (SELECT value FROM json_each(?))
2276
+ AND sn.file_path <> tn.file_path
2277
+ GROUP BY tn.file_path`)
2278
+ .all(JSON.stringify(filePaths));
2279
+ }
2280
+ /**
2281
+ * How far each of the given files reaches OUT: distinct other files its
2282
+ * symbols touch, and how many references that is.
2283
+ *
2284
+ * The mirror of {@link getFileDependentCounts}, and the same reasoning about
2285
+ * `contains` and same-file edges applies. It is driven from `nodes` rather
2286
+ * than from `edges` so the work is proportional to the files asked about —
2287
+ * the entry-points endpoint asks it about every test file in the index, and
2288
+ * an edge-first plan would scan the whole table to answer a question about a
2289
+ * tenth of it.
2290
+ */
2291
+ getFileReachCounts(filePaths) {
2292
+ if (filePaths.length === 0)
2293
+ return [];
2294
+ return this.db
2295
+ .prepare(`SELECT sn.file_path AS filePath,
2296
+ COUNT(DISTINCT tn.file_path) AS reaches,
2297
+ COUNT(*) AS refs
2298
+ FROM nodes sn
2299
+ JOIN edges e ON e.source = sn.id
2300
+ JOIN nodes tn ON tn.id = e.target
2301
+ WHERE sn.file_path IN (SELECT value FROM json_each(?))
2302
+ AND e.kind != 'contains'
2303
+ AND tn.file_path <> sn.file_path
2304
+ GROUP BY sn.file_path`)
2305
+ .all(JSON.stringify(filePaths));
2306
+ }
2307
+ /**
2308
+ * The `file` nodes for the given paths, in one query.
2309
+ *
2310
+ * A file's own node is what makes a file row navigable, and looking it up
2311
+ * with {@link getNodesInFile} means materialising every symbol in the file to
2312
+ * throw all but one away.
2313
+ */
2314
+ getFileNodes(filePaths) {
2315
+ if (filePaths.length === 0)
2316
+ return [];
2317
+ const rows = this.db
2318
+ .prepare(`SELECT * FROM nodes
2319
+ WHERE kind = 'file'
2320
+ AND file_path IN (SELECT value FROM json_each(?))`)
2321
+ .all(JSON.stringify(filePaths));
2322
+ return rows.map(rowToNode);
2323
+ }
2324
+ /**
2325
+ * Roll the whole edge table up to module granularity in one pass.
2326
+ *
2327
+ * The caller decides what a module IS — it hands in a file → module
2328
+ * assignment and gets back the cross-module traffic. That split is
2329
+ * deliberate: naming modules is a *policy* (top-level directories, a façade
2330
+ * file kept separate, a monorepo root) that belongs where the reader lives,
2331
+ * while grouping a million edges by it is *mechanics* that must happen in
2332
+ * SQLite. Doing the fold in JavaScript instead means materialising every
2333
+ * cross-file edge in memory; doing the naming in SQL means a tower of
2334
+ * `instr`/`substr` no one can read.
2335
+ *
2336
+ * The assignment lands in a TEMP table with a primary key, so the join is
2337
+ * indexed and the result set is bounded by modules², not by edges. Temp
2338
+ * tables live in SQLite's own temp database, so this stays valid against a
2339
+ * read-only main.
2340
+ *
2341
+ * Two result sets, because they need two different groupings over the same
2342
+ * join: `links` counts edges per (module, module, kind), and `pairs` names
2343
+ * the busiest symbol pairs behind each link (the map's tooltip). `pairs` is
2344
+ * ranked and cut inside SQLite — the un-cut grouping is the one thing here
2345
+ * that scales with distinct symbol names rather than with modules. Pairs are
2346
+ * ranked by `declared` before raw count, so a link's tooltip names the
2347
+ * symbols the source actually points at rather than whichever `has`/`get`
2348
+ * happened to name-match most often.
2349
+ *
2350
+ * `declared` is the subset of a link's edges that came from something the
2351
+ * source *writes down*: an import, a qualified name, an inheritance clause,
2352
+ * or a call through a typed receiver. It exists because bare name matching
2353
+ * (`resolvedBy: 'exact-match'`) is what invents cross-module links out of
2354
+ * common method names — `run`, `push`, `finish` — and a map that lets those
2355
+ * decide the layering puts the storage layer above the CLI.
2356
+ */
2357
+ aggregateModuleGraph(assignments, options) {
2358
+ if (assignments.length === 0 || options.kinds.length === 0)
2359
+ return { links: [], pairs: [] };
2360
+ const CONFIDENCE = `COALESCE(json_extract(e.metadata, '$.confidence'), 1)`;
2361
+ const DECLARED = `(json_extract(e.metadata, '$.resolvedBy') IN ('import', 'qualified-name')
2362
+ OR e.kind IN ('extends', 'implements')
2363
+ OR (json_extract(e.metadata, '$.resolvedBy') = 'instance-method'
2364
+ AND ${CONFIDENCE} >= 0.9))`;
2365
+ this.db.exec('DROP TABLE IF EXISTS temp.cg_module_map');
2366
+ this.db.exec('CREATE TEMP TABLE cg_module_map (path TEXT PRIMARY KEY, mod TEXT NOT NULL)');
2367
+ try {
2368
+ const insert = this.db.prepare('INSERT OR REPLACE INTO cg_module_map (path, mod) VALUES (?, ?)');
2369
+ this.db.exec('BEGIN');
2370
+ try {
2371
+ for (const row of assignments)
2372
+ insert.run(row.filePath, row.module);
2373
+ this.db.exec('COMMIT');
2374
+ }
2375
+ catch (err) {
2376
+ this.db.exec('ROLLBACK');
2377
+ throw err;
2378
+ }
2379
+ // ONE pass over the edge table. Grouping by the symbol names as well as
2380
+ // the modules costs nothing extra in scan time — the join is what is
2381
+ // expensive — and it buys both results from a single scan. Measured on
2382
+ // this index inflated to 1.6M edges: 1.66s for this query against 3.0s
2383
+ // for the module-level and name-level queries run separately, which is
2384
+ // the difference between meeting and missing the map's cold budget on a
2385
+ // ten-thousand-file repository.
2386
+ const rows = this.db
2387
+ .prepare(`SELECT ms.mod AS source, mt.mod AS target, e.kind AS kind,
2388
+ sn.name AS "from", tn.name AS "to",
2389
+ SUM(CASE WHEN ${CONFIDENCE} >= ? THEN 1 ELSE 0 END) AS count,
2390
+ SUM(CASE WHEN ${CONFIDENCE} >= ? AND ${DECLARED} THEN 1 ELSE 0 END) AS declared,
2391
+ SUM(CASE WHEN ${CONFIDENCE} < ? THEN 1 ELSE 0 END) AS uncertain
2392
+ FROM edges e
2393
+ JOIN nodes sn ON sn.id = e.source
2394
+ JOIN nodes tn ON tn.id = e.target
2395
+ JOIN cg_module_map ms ON ms.path = sn.file_path
2396
+ JOIN cg_module_map mt ON mt.path = tn.file_path
2397
+ WHERE e.kind IN (SELECT value FROM json_each(?))
2398
+ AND ms.mod <> mt.mod
2399
+ GROUP BY ms.mod, mt.mod, e.kind, sn.name, tn.name`)
2400
+ .all(options.minConfidence, options.minConfidence, options.minConfidence, JSON.stringify(options.kinds));
2401
+ return foldModuleRows(rows, options);
2402
+ }
2403
+ finally {
2404
+ this.db.exec('DROP TABLE IF EXISTS temp.cg_module_map');
2405
+ }
2406
+ }
2407
+ /**
2408
+ * Every ordered pair of files where one reaches into the other, once each.
2409
+ *
2410
+ * The input a cycle finder wants: file-level circular dependencies are the
2411
+ * strongly connected components of this graph. One query instead of the
2412
+ * dependency lookup per file that {@link GraphQueryManager.findCircularDependencies}
2413
+ * does — which matters because a cycle report is only interesting on a large
2414
+ * repo, and that is exactly where a query per file stops being affordable.
2415
+ *
2416
+ * `contains` is excluded (a file "contains" its own symbols, which is not a
2417
+ * dependency), and so are same-file edges and low-confidence name matches:
2418
+ * a cycle conjured by a common method name is a false alarm a reader cannot
2419
+ * check.
2420
+ */
2421
+ getCrossFileDependencyPairs(minConfidence) {
2422
+ return this.db
2423
+ .prepare(`SELECT DISTINCT sn.file_path AS source, tn.file_path AS target
2424
+ FROM edges e
2425
+ JOIN nodes sn ON sn.id = e.source
2426
+ JOIN nodes tn ON tn.id = e.target
2427
+ WHERE e.kind <> 'contains'
2428
+ AND sn.file_path <> tn.file_path
2429
+ AND COALESCE(json_extract(e.metadata, '$.confidence'), 1) >= ?`)
2430
+ .all(minConfidence);
2431
+ }
2432
+ /**
2433
+ * Every unresolved reference recorded in one FILE, ordered by line.
2434
+ *
2435
+ * The per-symbol form above answers "what does this body reach that the
2436
+ * index does not hold". A whole-file reader asks the same question of every
2437
+ * line at once, and asking it one symbol at a time is a query per symbol —
2438
+ * 153 of them on this repo's largest file. `unresolved_refs.file_path` is
2439
+ * indexed, so this is one lookup whatever the file holds.
2440
+ *
2441
+ * `limit` bounds the answer rather than the work: the caller draws a marker
2442
+ * per row, and a generated file with fifty thousand of them would ship
2443
+ * megabytes to say something a count already says. Rows come back in line
2444
+ * order, so a cap trims the END of the file, which is at least legible.
2445
+ */
2446
+ getUnresolvedReferencesInFile(filePath, limit = 5000) {
2447
+ if (!this.stmts.getUnresolvedInFile) {
2448
+ this.stmts.getUnresolvedInFile = this.db.prepare('SELECT * FROM unresolved_refs WHERE file_path = ? ORDER BY line, col LIMIT ?');
2449
+ }
2450
+ const rows = this.stmts.getUnresolvedInFile.all(filePath, limit);
2451
+ return rows.map((row) => ({
2452
+ fromNodeId: row.from_node_id,
2453
+ referenceName: row.reference_name,
2454
+ referenceKind: row.reference_kind,
2455
+ line: row.line,
2456
+ column: row.col,
2457
+ candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
2458
+ filePath: row.file_path,
2459
+ language: row.language,
2460
+ rowId: row.id,
2461
+ }));
2462
+ }
2463
+ /**
2464
+ * References recorded against a symbol that never resolved to a node — the
2465
+ * calls and type mentions that leave the index (a third-party package, a
2466
+ * runtime builtin, a language construct extraction doesn't model).
2467
+ *
2468
+ * Read-only. It exists so a reader can say "N calls into symbols outside the
2469
+ * index" instead of silently showing a callee list shorter than the body's
2470
+ * call sites, which reads as "nothing else happens here".
2471
+ */
2472
+ getUnresolvedReferencesFrom(fromNodeId) {
2473
+ if (!this.stmts.getUnresolvedFromNode) {
2474
+ // Insertion (extraction) order, stated: without ORDER BY the row order
2475
+ // follows whichever index the planner picks — (from_node_id,
2476
+ // reference_name) returns name order — and readers that stable-sort by
2477
+ // position then break ties differently from build to build.
2478
+ this.stmts.getUnresolvedFromNode = this.db.prepare('SELECT * FROM unresolved_refs WHERE from_node_id = ? ORDER BY id');
2479
+ }
2480
+ const rows = this.stmts.getUnresolvedFromNode.all(fromNodeId);
2481
+ return rows.map((row) => ({
2482
+ fromNodeId: row.from_node_id,
2483
+ referenceName: row.reference_name,
2484
+ referenceKind: row.reference_kind,
2485
+ line: row.line,
2486
+ column: row.col,
2487
+ candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
2488
+ filePath: row.file_path,
2489
+ language: row.language,
2490
+ rowId: row.id,
2491
+ }));
2492
+ }
1540
2493
  /**
1541
2494
  * Find all edges where both source and target are in the given node set.
1542
2495
  * Useful for recovering inter-node connectivity after BFS.
@@ -1647,8 +2600,8 @@ class QueryBuilder {
1647
2600
  upsertFile(file) {
1648
2601
  if (!this.stmts.upsertFile) {
1649
2602
  this.stmts.upsertFile = this.db.prepare(`
1650
- INSERT INTO files (path, content_hash, language, size, modified_at, indexed_at, node_count, extraction_version, errors)
1651
- VALUES (@path, @contentHash, @language, @size, @modifiedAt, @indexedAt, @nodeCount, @extractionVersion, @errors)
2603
+ INSERT INTO files (path, content_hash, language, size, modified_at, indexed_at, node_count, extraction_version, errors, generated)
2604
+ VALUES (@path, @contentHash, @language, @size, @modifiedAt, @indexedAt, @nodeCount, @extractionVersion, @errors, @generated)
1652
2605
  ON CONFLICT(path) DO UPDATE SET
1653
2606
  content_hash = @contentHash,
1654
2607
  language = @language,
@@ -1657,7 +2610,8 @@ class QueryBuilder {
1657
2610
  indexed_at = @indexedAt,
1658
2611
  node_count = @nodeCount,
1659
2612
  extraction_version = @extractionVersion,
1660
- errors = @errors
2613
+ errors = @errors,
2614
+ generated = @generated
1661
2615
  `);
1662
2616
  }
1663
2617
  this.stmts.upsertFile.run({
@@ -1670,6 +2624,9 @@ class QueryBuilder {
1670
2624
  nodeCount: file.nodeCount,
1671
2625
  extractionVersion: file.extractionVersion,
1672
2626
  errors: file.errors ? JSON.stringify(file.errors) : null,
2627
+ // The upsert always REWRITES the flag: a file that loses its banner in an
2628
+ // edit must lose the flag on the next sync, not keep a stale 1.
2629
+ generated: file.generated ? 1 : 0,
1673
2630
  });
1674
2631
  }
1675
2632
  /**
@@ -1711,6 +2668,173 @@ class QueryBuilder {
1711
2668
  .get(currentVersion);
1712
2669
  return row.ahead;
1713
2670
  }
2671
+ /**
2672
+ * Which of `filePaths` the index flagged as tool-generated (schema v9+).
2673
+ *
2674
+ * Bounded-lookup by design: every consumer already holds a short candidate
2675
+ * list (a ranked file group, an FTS result page, a LIMIT-20 aggregate), so
2676
+ * this stays a partial-index probe over a handful of paths — no whole-repo
2677
+ * set to materialize, and no cache to invalidate, which means a ranking call
2678
+ * can never serve a verdict the last sync already replaced.
2679
+ *
2680
+ * Returns ONLY the content/index signal; callers union it with
2681
+ * {@link isGeneratedFile} so pre-v9 databases (column present, all zeros
2682
+ * until a re-index) keep the path-only behavior rather than regressing.
2683
+ */
2684
+ getGeneratedPathsAmong(filePaths) {
2685
+ const unique = [...new Set(filePaths)];
2686
+ const found = new Set();
2687
+ if (unique.length === 0)
2688
+ return found;
2689
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
2690
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
2691
+ const placeholders = chunk.map(() => '?').join(',');
2692
+ const rows = this.db
2693
+ .prepare(`SELECT path FROM files WHERE generated = 1 AND path IN (${placeholders})`)
2694
+ .all(...chunk);
2695
+ for (const row of rows)
2696
+ found.add(row.path);
2697
+ }
2698
+ return found;
2699
+ }
2700
+ /**
2701
+ * A reusable `(path) => boolean` over a bounded candidate list, unioning the
2702
+ * indexed flag with the path convention. This is the shape every ranking
2703
+ * comparator wants: one query up front, then O(1) per comparison.
2704
+ */
2705
+ generatedPredicateFor(filePaths) {
2706
+ const flagged = this.getGeneratedPathsAmong(filePaths);
2707
+ return (filePath) => flagged.has(filePath) || (0, generated_detection_1.isGeneratedFile)(filePath);
2708
+ }
2709
+ /**
2710
+ * Which of `filePaths` are AMBIENT DECLARATION files — they declare nothing
2711
+ * but types, and nothing in the index depends on them (CG-28). A hand-written
2712
+ * ambient `.d.ts` of global shims, a vendored typings file, module
2713
+ * augmentation: reachable only by name, structurally attached to nothing.
2714
+ *
2715
+ * Structural, not extension-based, so a hand-written `types.ts` and a `.d.ts`
2716
+ * are judged by the same rule and a `.d.ts` that does declare a class or a
2717
+ * const is (correctly) not caught. Four conditions, all required:
2718
+ *
2719
+ * 1. it declares at least one symbol — an empty or unparsed file is not a
2720
+ * declaration file, it is a file we know nothing about;
2721
+ * 2. EVERY declared symbol is a type-level kind (interface / type alias /
2722
+ * enum / namespace). The narrowness is deliberate and measured: a rule
2723
+ * of "no callables" alone flags 1–18% of a repo, including Kotlin sealed
2724
+ * classes, Rust `mod.rs` re-exports and django's locale constant tables —
2725
+ * real source that must not be demoted. This rule flags 0–4%;
2726
+ * 3. no symbol in it originates a `calls`/`instantiates` edge — the direct
2727
+ * evidence that nothing here has a body;
2728
+ * 4. NOTHING ELSE IN THE INDEX points at it. This is the condition that
2729
+ * separates an ambient shim from a working type module, and it is why
2730
+ * the flag is narrow enough to be safe: `displacement-ts`'s pipeline
2731
+ * `types.ts` passes 1–3 identically but carries 13 inbound imports and
2732
+ * 21 references, so the files that answer a query about the pipeline are
2733
+ * typed BY it — it is part of that answer's structure. An ambient
2734
+ * `declare global` shim has zero. Deliberately index-wide rather than
2735
+ * restricted to the candidate list: the file that imports it is usually
2736
+ * not itself a candidate.
2737
+ *
2738
+ * ### Interface MEMBERS are transparent to all four conditions
2739
+ *
2740
+ * A `method_signature` / `property_signature` inside an interface enters the
2741
+ * graph as a `method` / `property` node (#1638). Read literally that would
2742
+ * break every condition here at once: condition 2 sees non-type kinds and
2743
+ * stops flagging, and — worse, because it is silent — condition 4 starts
2744
+ * seeing inbound `calls` edges the moment a call site through the shim's API
2745
+ * finally has a signature to land on. An ambient `.d.ts` would quietly lose
2746
+ * its damping precisely BECAUSE the platform API it declares is widely used.
2747
+ *
2748
+ * So an interface-owned member is treated the way `parameter` already is: it
2749
+ * neither qualifies, disqualifies, nor counts as inbound dependency. That is
2750
+ * not a new judgement call, it is what keeps the rule measuring what it was
2751
+ * measured on — before #1638 these nodes did not exist, so excluding them
2752
+ * reproduces the 0–4% flag rate the thresholds above were tuned against. It
2753
+ * is also the semantically right answer: a signature with no body is on the
2754
+ * same side of the line as the interface that owns it, and a call edge
2755
+ * landing on one is still not a file that can answer a flow question.
2756
+ *
2757
+ * The interface ITSELF is untouched: the `references` edges an importing
2758
+ * module aims at `UploadStorage` still disqualify the file under (4), which
2759
+ * is what keeps a depended-on `types.ts` out of the flag.
2760
+ *
2761
+ * Bounded-lookup like {@link getGeneratedPathsAmong}: callers hold a ranked
2762
+ * candidate list, so this is a partial-index probe over a handful of paths.
2763
+ */
2764
+ getAmbientDeclarationPathsAmong(filePaths) {
2765
+ const unique = [...new Set(filePaths)];
2766
+ const found = new Set();
2767
+ if (unique.length === 0)
2768
+ return found;
2769
+ for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
2770
+ const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
2771
+ const placeholders = chunk.map(() => '?').join(',');
2772
+ // `file`/`import`/`export`/`parameter` are structural bookkeeping, not
2773
+ // things the file declares, so they neither qualify nor disqualify.
2774
+ const rows = this.db
2775
+ .prepare(`
2776
+ SELECT n.file_path AS file_path,
2777
+ SUM(CASE WHEN n.kind NOT IN ('file','import','export','parameter')
2778
+ AND NOT ${IS_INTERFACE_MEMBER('n')}
2779
+ THEN 1 ELSE 0 END) AS declared,
2780
+ SUM(CASE WHEN n.kind IN ('interface','type_alias','enum','enum_member','namespace')
2781
+ THEN 1 ELSE 0 END) AS typeDeclared
2782
+ FROM nodes n
2783
+ WHERE n.file_path IN (${placeholders})
2784
+ GROUP BY n.file_path
2785
+ `)
2786
+ .all(...chunk);
2787
+ let candidates = rows
2788
+ .filter((r) => r.declared > 0 && r.declared === r.typeDeclared)
2789
+ .map((r) => r.file_path);
2790
+ if (candidates.length === 0)
2791
+ continue;
2792
+ const disqualify = (sql) => {
2793
+ if (candidates.length === 0)
2794
+ return;
2795
+ const hit = new Set(this.db
2796
+ .prepare(sql.replace('$IN$', candidates.map(() => '?').join(',')))
2797
+ .all(...candidates).map((r) => r.file_path));
2798
+ candidates = candidates.filter((p) => !hit.has(p));
2799
+ };
2800
+ // (3) originates behaviour — a signature has no body to originate from,
2801
+ // so an edge attributed to one is not evidence about this file.
2802
+ disqualify(`
2803
+ SELECT DISTINCT n.file_path AS file_path
2804
+ FROM edges e JOIN nodes n ON n.id = e.source
2805
+ WHERE e.kind IN ('calls','instantiates') AND n.file_path IN ($IN$)
2806
+ AND NOT ${IS_INTERFACE_MEMBER('n')}
2807
+ `);
2808
+ // (4) something outside the file depends on it — but a call that lands on
2809
+ // an interface's own signature is a use of the API, not a dependency on
2810
+ // this file's structure. The edges aimed at the interface still count.
2811
+ disqualify(`
2812
+ SELECT DISTINCT t.file_path AS file_path
2813
+ FROM edges e JOIN nodes t ON t.id = e.target JOIN nodes s ON s.id = e.source
2814
+ WHERE t.file_path IN ($IN$) AND s.file_path <> t.file_path
2815
+ AND NOT ${IS_INTERFACE_MEMBER('t')}
2816
+ `);
2817
+ for (const path of candidates)
2818
+ found.add(path);
2819
+ }
2820
+ return found;
2821
+ }
2822
+ /**
2823
+ * A reusable `(path) => boolean` ambient-declaration test over a bounded
2824
+ * candidate list — the shape a ranking comparator wants: one query up front,
2825
+ * O(1) per comparison.
2826
+ */
2827
+ ambientDeclarationPredicateFor(filePaths) {
2828
+ const flagged = this.getAmbientDeclarationPathsAmong(filePaths);
2829
+ return (filePath) => flagged.has(filePath);
2830
+ }
2831
+ /** How many indexed files carry the generated flag. Surfaced by `status`. */
2832
+ countGeneratedFiles() {
2833
+ const row = this.db
2834
+ .prepare('SELECT COUNT(*) AS n FROM files WHERE generated = 1')
2835
+ .get();
2836
+ return row?.n ?? 0;
2837
+ }
1714
2838
  /**
1715
2839
  * Delete a file record and its nodes
1716
2840
  */
@@ -1753,6 +2877,40 @@ class QueryBuilder {
1753
2877
  .get();
1754
2878
  return row?.last ?? null;
1755
2879
  }
2880
+ /**
2881
+ * The index's revision marker: how far the last sync got, and how many files
2882
+ * it left behind — one query, both numbers.
2883
+ *
2884
+ * This is the cheapest honest answer to "has the index moved since I last
2885
+ * looked". `MAX(indexed_at)` alone is not enough: a sync that only DELETES
2886
+ * files (a branch checkout that removed a directory) advances nothing, and
2887
+ * the graph the viewer is showing has still changed underneath it. The row
2888
+ * count catches exactly that case.
2889
+ */
2890
+ getIndexRevision() {
2891
+ const row = this.db
2892
+ .prepare('SELECT MAX(indexed_at) AS last, COUNT(*) AS files FROM files')
2893
+ .get();
2894
+ return { lastIndexedAt: row?.last ?? null, fileCount: row?.files ?? 0 };
2895
+ }
2896
+ /**
2897
+ * Files re-indexed strictly after `since` (ms since epoch), newest first.
2898
+ *
2899
+ * `total` is the real count; `paths` is capped at `limit`. Used by the
2900
+ * viewer's live channel to name what a sync just picked up. A file the same
2901
+ * sync DELETED cannot appear here — it has no row left — which is why the
2902
+ * caller compares {@link getIndexRevision} as well rather than treating an
2903
+ * empty list as "nothing happened".
2904
+ */
2905
+ getFilesIndexedSince(since, limit) {
2906
+ const count = this.db
2907
+ .prepare('SELECT COUNT(*) AS n FROM files WHERE indexed_at > ?')
2908
+ .get(since);
2909
+ const rows = this.db
2910
+ .prepare('SELECT path FROM files WHERE indexed_at > ? ORDER BY indexed_at DESC, path LIMIT ?')
2911
+ .all(since, Math.max(0, limit));
2912
+ return { paths: rows.map((r) => r.path), total: count?.n ?? rows.length };
2913
+ }
1756
2914
  /**
1757
2915
  * Get files that need re-indexing (hash changed)
1758
2916
  */
@@ -1917,11 +3075,19 @@ class QueryBuilder {
1917
3075
  * (§7a.2) — while the seek is O(batch) forever. `id` is the rowid alias, so
1918
3076
  * the enumeration order is identical to the OFFSET reader's.
1919
3077
  */
1920
- getUnresolvedReferencesBatchAfter(afterRowId, limit) {
1921
- if (!this.stmts.getUnresolvedBatchAfter) {
1922
- this.stmts.getUnresolvedBatchAfter = this.db.prepare("SELECT * FROM unresolved_refs WHERE status = 'pending' AND id > ? ORDER BY id LIMIT ?");
1923
- }
1924
- const rows = this.stmts.getUnresolvedBatchAfter.all(afterRowId, limit);
3078
+ getUnresolvedReferencesBatchAfter(afterRowId, limit, prerequisites) {
3079
+ // Resolution prerequisites must be committed before dependent calls,
3080
+ // even when an interrupted sync queued their rows in a different order
3081
+ // from a clean index (#1577). Each phase still seeks by row id in bounded
3082
+ // memory; the default preserves the public reader's original enumeration.
3083
+ const key = prerequisites === undefined ? 'getUnresolvedBatchAfter'
3084
+ : prerequisites ? 'getUnresolvedPrerequisitesAfter' : 'getUnresolvedDependentsAfter';
3085
+ if (!this.stmts[key]) {
3086
+ const filter = prerequisites === undefined ? ''
3087
+ : ` AND reference_kind ${prerequisites ? 'IN' : 'NOT IN'} ('imports', 'extends', 'implements')`;
3088
+ this.stmts[key] = this.db.prepare(`SELECT * FROM unresolved_refs WHERE status = 'pending' AND id > ?${filter} ORDER BY id LIMIT ?`);
3089
+ }
3090
+ const rows = this.stmts[key].all(afterRowId, limit);
1925
3091
  return rows.map((row) => ({
1926
3092
  fromNodeId: row.from_node_id,
1927
3093
  referenceName: row.reference_name,
@@ -1991,7 +3157,13 @@ class QueryBuilder {
1991
3157
  const chunkRows = this.db
1992
3158
  .prepare(`SELECT * FROM unresolved_refs WHERE status = 'pending' AND file_path IN (${placeholders})`)
1993
3159
  .all(...chunk);
1994
- rows.push(...chunkRows);
3160
+ // Append with a loop, never a spread: the INPUT chunk is bounded, but
3161
+ // the RESULT rows per chunk are not — a dense recovery sync (e.g. the
3162
+ // #1541 self-heal re-indexing hundreds of files) returns more rows than
3163
+ // V8 allows as arguments, and `push(...chunkRows)` dies with "Maximum
3164
+ // call stack size exceeded", aborting resolution mid-sync (#1558).
3165
+ for (const row of chunkRows)
3166
+ rows.push(row);
1995
3167
  }
1996
3168
  return rows.map((row) => ({
1997
3169
  fromNodeId: row.from_node_id,
@@ -2191,7 +3363,11 @@ class QueryBuilder {
2191
3363
  const chunkRows = this.db
2192
3364
  .prepare(`SELECT * FROM unresolved_refs WHERE status = 'failed' AND name_tail IN (${placeholders})`)
2193
3365
  .all(...chunk);
2194
- rows.push(...chunkRows);
3366
+ // Loop, not spread — same V8 argument-limit hazard as
3367
+ // getUnresolvedReferencesByFiles (#1558): a large definition delta can
3368
+ // select an unbounded number of failed rows per chunk.
3369
+ for (const row of chunkRows)
3370
+ rows.push(row);
2195
3371
  }
2196
3372
  return rows.map((row) => ({
2197
3373
  fromNodeId: row.from_node_id,
@@ -2207,6 +3383,103 @@ class QueryBuilder {
2207
3383
  importedName: row.imported_name ?? undefined,
2208
3384
  }));
2209
3385
  }
3386
+ /**
3387
+ * Resolution edges whose TARGET symbol is named one of `names` — the edges a
3388
+ * sync must re-resolve after `names` gained or lost a definition (CG-33).
3389
+ *
3390
+ * Resolution binds a reference to a node whose name matches the reference's
3391
+ * tail, and it picks among ALL same-named definitions project-wide. So adding
3392
+ * or removing one definition of `pct` changes the answer for every `pct(...)`
3393
+ * reference in the repo — including references in files this sync never
3394
+ * touches, whose edges nothing else revisits. Those edges' current target is,
3395
+ * by that same rule, a node named `pct`, which is why the target's name is a
3396
+ * sufficient (and index-backed, via idx_nodes_name) way to find them without
3397
+ * a schema change or a scan of edge metadata.
3398
+ *
3399
+ * Returns the source file/language alongside each edge so the caller can
3400
+ * resurrect it as its original reference. Excludes `provenance='heuristic'`
3401
+ * (synthesized dispatch edges are not resolution output and carry no refName
3402
+ * stamp to resurrect from — deleting one would be a permanent loss).
3403
+ *
3404
+ * Names matching more than `perNameCeiling` edges are skipped entirely, same
3405
+ * rationale and same default as {@link getRetryableFailedReferences}: at that
3406
+ * population the name is generic (`get`, `clear`, …), one definition changing
3407
+ * won't flip most of them, and rebinding an arbitrary subset is both wasted
3408
+ * work and incoherent coverage.
3409
+ */
3410
+ getResolutionEdgesByTargetName(names, perNameCeiling = 500) {
3411
+ if (names.length === 0)
3412
+ return [];
3413
+ // Pass 1: per-name edge counts, chunked under the SQLite parameter limit.
3414
+ const keep = [];
3415
+ for (let i = 0; i < names.length; i += SQLITE_PARAM_CHUNK_SIZE) {
3416
+ const chunk = names.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
3417
+ const placeholders = chunk.map(() => '?').join(',');
3418
+ const counts = this.db
3419
+ .prepare(`SELECT tgt.name AS name, COUNT(*) AS count
3420
+ FROM edges e
3421
+ JOIN nodes tgt ON tgt.id = e.target
3422
+ WHERE tgt.name IN (${placeholders})
3423
+ AND (e.provenance IS NULL OR e.provenance != 'heuristic')
3424
+ GROUP BY tgt.name`)
3425
+ .all(...chunk);
3426
+ for (const row of counts) {
3427
+ if (row.count <= perNameCeiling)
3428
+ keep.push(row.name);
3429
+ }
3430
+ }
3431
+ if (keep.length === 0)
3432
+ return [];
3433
+ // Pass 2: load the surviving edges with the source file context a
3434
+ // resurrection needs.
3435
+ const out = [];
3436
+ for (let i = 0; i < keep.length; i += SQLITE_PARAM_CHUNK_SIZE) {
3437
+ const chunk = keep.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
3438
+ const placeholders = chunk.map(() => '?').join(',');
3439
+ const rows = this.db
3440
+ .prepare(`SELECT e.*, src.file_path AS source_file_path, src.language AS source_language
3441
+ FROM edges e
3442
+ JOIN nodes tgt ON tgt.id = e.target
3443
+ JOIN nodes src ON src.id = e.source
3444
+ WHERE tgt.name IN (${placeholders})
3445
+ AND (e.provenance IS NULL OR e.provenance != 'heuristic')`)
3446
+ .all(...chunk);
3447
+ for (const row of rows) {
3448
+ out.push({
3449
+ ...rowToEdge(row),
3450
+ edgeId: row.id,
3451
+ sourceFilePath: row.source_file_path,
3452
+ sourceLanguage: row.source_language,
3453
+ });
3454
+ }
3455
+ }
3456
+ return out;
3457
+ }
3458
+ /** Delete edges by primary key — the rebind pass's half of a re-resolution. */
3459
+ deleteEdgesByIds(edgeIds) {
3460
+ if (edgeIds.length === 0)
3461
+ return 0;
3462
+ let changed = 0;
3463
+ this.db.transaction(() => {
3464
+ for (let i = 0; i < edgeIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
3465
+ const chunk = edgeIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
3466
+ const placeholders = chunk.map(() => '?').join(',');
3467
+ changed += this.db.prepare(`DELETE FROM edges WHERE id IN (${placeholders})`).run(...chunk).changes;
3468
+ }
3469
+ })();
3470
+ return changed;
3471
+ }
3472
+ /**
3473
+ * Replace resolution edges with their original unresolved references as one
3474
+ * transaction. If ref insertion fails, the edge deletion is rolled back.
3475
+ */
3476
+ replaceResolutionEdgesWithUnresolvedRefs(edgeIds, refs) {
3477
+ return this.db.transaction(() => {
3478
+ const changed = this.deleteEdgesByIds(edgeIds);
3479
+ this.insertUnresolvedRefsBatch(refs);
3480
+ return changed;
3481
+ })();
3482
+ }
2210
3483
  /**
2211
3484
  * Distinct node names present in the given files — the symbol names a sync
2212
3485
  * pass uses to look up retryable failed refs after those files changed.
@@ -2226,6 +3499,34 @@ class QueryBuilder {
2226
3499
  }
2227
3500
  return [...names];
2228
3501
  }
3502
+ /**
3503
+ * Distinct `file\0name` pairs defined by the given files — the shape sync's
3504
+ * definition delta needs (CG-33).
3505
+ *
3506
+ * Deliberately NOT `getNodeNamesByFiles`: a bare name set is taken over the
3507
+ * WHOLE changed batch, so a name that moves between two files in one commit
3508
+ * (or exists in one changed file and is newly added to another) appears on
3509
+ * both sides and cancels out of the symmetric difference — even though a
3510
+ * definition genuinely appeared or vanished and every reference to that name
3511
+ * repo-wide may now bind elsewhere. Keying by file makes each definition its
3512
+ * own fact, so the move is seen as one removal plus one addition.
3513
+ */
3514
+ getNodeNamePairsByFiles(filePaths) {
3515
+ const pairs = new Set();
3516
+ if (filePaths.length === 0)
3517
+ return pairs;
3518
+ for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
3519
+ const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
3520
+ const placeholders = chunk.map(() => '?').join(',');
3521
+ const rows = this.db
3522
+ .prepare(`SELECT DISTINCT file_path, name FROM nodes WHERE file_path IN (${placeholders})`)
3523
+ .all(...chunk);
3524
+ // NUL-joined: a path or a symbol name can contain a space, never a NUL.
3525
+ for (const row of rows)
3526
+ pairs.add(`${row.file_path}\0${row.name}`);
3527
+ }
3528
+ return pairs;
3529
+ }
2229
3530
  // ===========================================================================
2230
3531
  // Statistics
2231
3532
  // ===========================================================================
@@ -2326,4 +3627,71 @@ class QueryBuilder {
2326
3627
  }
2327
3628
  }
2328
3629
  exports.QueryBuilder = QueryBuilder;
3630
+ function foldModuleRows(rows, options) {
3631
+ // A module id is a path and may contain anything printable, so the key
3632
+ // separator has to be something a path cannot hold.
3633
+ const SEP = '\u0000';
3634
+ const links = new Map();
3635
+ const pairKinds = new Set(options.pairKinds);
3636
+ const wantPairs = options.topPairsPerLink > 0 && pairKinds.size > 0;
3637
+ const pairTotals = new Map();
3638
+ for (const row of rows) {
3639
+ const linkKey = `${row.source}${SEP}${row.target}${SEP}${row.kind}`;
3640
+ const link = links.get(linkKey);
3641
+ if (link) {
3642
+ link.count += row.count;
3643
+ link.declared += row.declared;
3644
+ link.uncertain += row.uncertain;
3645
+ }
3646
+ else {
3647
+ links.set(linkKey, {
3648
+ source: row.source,
3649
+ target: row.target,
3650
+ kind: row.kind,
3651
+ count: row.count,
3652
+ declared: row.declared,
3653
+ uncertain: row.uncertain,
3654
+ });
3655
+ }
3656
+ // Only the confident half of a row can be named: an uncertain edge is a
3657
+ // guess, and printing "a to b, 12" for twelve guesses is the map claiming
3658
+ // something it does not know.
3659
+ if (!wantPairs || row.count === 0 || !pairKinds.has(row.kind))
3660
+ continue;
3661
+ const pairKey = `${row.source}${SEP}${row.target}${SEP}${row.from}${SEP}${row.to}`;
3662
+ const pair = pairTotals.get(pairKey);
3663
+ if (pair) {
3664
+ pair.count += row.count;
3665
+ pair.declared += row.declared;
3666
+ }
3667
+ else {
3668
+ pairTotals.set(pairKey, {
3669
+ source: row.source,
3670
+ target: row.target,
3671
+ from: row.from,
3672
+ to: row.to,
3673
+ count: row.count,
3674
+ declared: row.declared,
3675
+ });
3676
+ }
3677
+ }
3678
+ const byLink = new Map();
3679
+ for (const pair of pairTotals.values()) {
3680
+ const key = `${pair.source}${SEP}${pair.target}`;
3681
+ let list = byLink.get(key);
3682
+ if (!list)
3683
+ byLink.set(key, (list = []));
3684
+ list.push(pair);
3685
+ }
3686
+ const pairs = [];
3687
+ for (const list of byLink.values()) {
3688
+ list.sort((a, b) => b.declared - a.declared ||
3689
+ b.count - a.count ||
3690
+ a.from.localeCompare(b.from) ||
3691
+ a.to.localeCompare(b.to));
3692
+ for (const pair of list.slice(0, options.topPairsPerLink))
3693
+ pairs.push(pair);
3694
+ }
3695
+ return { links: [...links.values()], pairs };
3696
+ }
2329
3697
  //# sourceMappingURL=queries.js.map