@stupidloud/codegraph 0.7.7

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 (340) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +482 -0
  3. package/dist/bin/codegraph.d.ts +21 -0
  4. package/dist/bin/codegraph.d.ts.map +1 -0
  5. package/dist/bin/codegraph.js +1296 -0
  6. package/dist/bin/codegraph.js.map +1 -0
  7. package/dist/bin/node-version-check.d.ts +17 -0
  8. package/dist/bin/node-version-check.d.ts.map +1 -0
  9. package/dist/bin/node-version-check.js +39 -0
  10. package/dist/bin/node-version-check.js.map +1 -0
  11. package/dist/bin/uninstall.d.ts +14 -0
  12. package/dist/bin/uninstall.d.ts.map +1 -0
  13. package/dist/bin/uninstall.js +148 -0
  14. package/dist/bin/uninstall.js.map +1 -0
  15. package/dist/config.d.ts +51 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +342 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/context/formatter.d.ts +30 -0
  20. package/dist/context/formatter.d.ts.map +1 -0
  21. package/dist/context/formatter.js +244 -0
  22. package/dist/context/formatter.js.map +1 -0
  23. package/dist/context/index.d.ts +99 -0
  24. package/dist/context/index.d.ts.map +1 -0
  25. package/dist/context/index.js +1075 -0
  26. package/dist/context/index.js.map +1 -0
  27. package/dist/db/index.d.ts +72 -0
  28. package/dist/db/index.d.ts.map +1 -0
  29. package/dist/db/index.js +200 -0
  30. package/dist/db/index.js.map +1 -0
  31. package/dist/db/migrations.d.ts +44 -0
  32. package/dist/db/migrations.d.ts.map +1 -0
  33. package/dist/db/migrations.js +160 -0
  34. package/dist/db/migrations.js.map +1 -0
  35. package/dist/db/queries.d.ts +253 -0
  36. package/dist/db/queries.d.ts.map +1 -0
  37. package/dist/db/queries.js +1202 -0
  38. package/dist/db/queries.js.map +1 -0
  39. package/dist/db/schema.sql +169 -0
  40. package/dist/db/sqlite-adapter.d.ts +52 -0
  41. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  42. package/dist/db/sqlite-adapter.js +237 -0
  43. package/dist/db/sqlite-adapter.js.map +1 -0
  44. package/dist/directory.d.ts +57 -0
  45. package/dist/directory.d.ts.map +1 -0
  46. package/dist/directory.js +264 -0
  47. package/dist/directory.js.map +1 -0
  48. package/dist/errors.d.ts +136 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +219 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/extraction/dfm-extractor.d.ts +31 -0
  53. package/dist/extraction/dfm-extractor.d.ts.map +1 -0
  54. package/dist/extraction/dfm-extractor.js +151 -0
  55. package/dist/extraction/dfm-extractor.js.map +1 -0
  56. package/dist/extraction/grammars.d.ts +78 -0
  57. package/dist/extraction/grammars.d.ts.map +1 -0
  58. package/dist/extraction/grammars.js +322 -0
  59. package/dist/extraction/grammars.js.map +1 -0
  60. package/dist/extraction/index.d.ts +130 -0
  61. package/dist/extraction/index.d.ts.map +1 -0
  62. package/dist/extraction/index.js +1279 -0
  63. package/dist/extraction/index.js.map +1 -0
  64. package/dist/extraction/languages/c-cpp.d.ts +4 -0
  65. package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
  66. package/dist/extraction/languages/c-cpp.js +126 -0
  67. package/dist/extraction/languages/c-cpp.js.map +1 -0
  68. package/dist/extraction/languages/csharp.d.ts +3 -0
  69. package/dist/extraction/languages/csharp.d.ts.map +1 -0
  70. package/dist/extraction/languages/csharp.js +72 -0
  71. package/dist/extraction/languages/csharp.js.map +1 -0
  72. package/dist/extraction/languages/dart.d.ts +3 -0
  73. package/dist/extraction/languages/dart.d.ts.map +1 -0
  74. package/dist/extraction/languages/dart.js +192 -0
  75. package/dist/extraction/languages/dart.js.map +1 -0
  76. package/dist/extraction/languages/go.d.ts +3 -0
  77. package/dist/extraction/languages/go.d.ts.map +1 -0
  78. package/dist/extraction/languages/go.js +58 -0
  79. package/dist/extraction/languages/go.js.map +1 -0
  80. package/dist/extraction/languages/index.d.ts +10 -0
  81. package/dist/extraction/languages/index.d.ts.map +1 -0
  82. package/dist/extraction/languages/index.js +45 -0
  83. package/dist/extraction/languages/index.js.map +1 -0
  84. package/dist/extraction/languages/java.d.ts +3 -0
  85. package/dist/extraction/languages/java.d.ts.map +1 -0
  86. package/dist/extraction/languages/java.js +64 -0
  87. package/dist/extraction/languages/java.js.map +1 -0
  88. package/dist/extraction/languages/javascript.d.ts +3 -0
  89. package/dist/extraction/languages/javascript.d.ts.map +1 -0
  90. package/dist/extraction/languages/javascript.js +90 -0
  91. package/dist/extraction/languages/javascript.js.map +1 -0
  92. package/dist/extraction/languages/kotlin.d.ts +3 -0
  93. package/dist/extraction/languages/kotlin.d.ts.map +1 -0
  94. package/dist/extraction/languages/kotlin.js +253 -0
  95. package/dist/extraction/languages/kotlin.js.map +1 -0
  96. package/dist/extraction/languages/pascal.d.ts +3 -0
  97. package/dist/extraction/languages/pascal.d.ts.map +1 -0
  98. package/dist/extraction/languages/pascal.js +66 -0
  99. package/dist/extraction/languages/pascal.js.map +1 -0
  100. package/dist/extraction/languages/php.d.ts +3 -0
  101. package/dist/extraction/languages/php.d.ts.map +1 -0
  102. package/dist/extraction/languages/php.js +107 -0
  103. package/dist/extraction/languages/php.js.map +1 -0
  104. package/dist/extraction/languages/python.d.ts +3 -0
  105. package/dist/extraction/languages/python.d.ts.map +1 -0
  106. package/dist/extraction/languages/python.js +56 -0
  107. package/dist/extraction/languages/python.js.map +1 -0
  108. package/dist/extraction/languages/ruby.d.ts +3 -0
  109. package/dist/extraction/languages/ruby.d.ts.map +1 -0
  110. package/dist/extraction/languages/ruby.js +114 -0
  111. package/dist/extraction/languages/ruby.js.map +1 -0
  112. package/dist/extraction/languages/rust.d.ts +3 -0
  113. package/dist/extraction/languages/rust.d.ts.map +1 -0
  114. package/dist/extraction/languages/rust.js +109 -0
  115. package/dist/extraction/languages/rust.js.map +1 -0
  116. package/dist/extraction/languages/scala.d.ts +3 -0
  117. package/dist/extraction/languages/scala.d.ts.map +1 -0
  118. package/dist/extraction/languages/scala.js +139 -0
  119. package/dist/extraction/languages/scala.js.map +1 -0
  120. package/dist/extraction/languages/swift.d.ts +3 -0
  121. package/dist/extraction/languages/swift.d.ts.map +1 -0
  122. package/dist/extraction/languages/swift.js +91 -0
  123. package/dist/extraction/languages/swift.js.map +1 -0
  124. package/dist/extraction/languages/typescript.d.ts +3 -0
  125. package/dist/extraction/languages/typescript.d.ts.map +1 -0
  126. package/dist/extraction/languages/typescript.js +129 -0
  127. package/dist/extraction/languages/typescript.js.map +1 -0
  128. package/dist/extraction/liquid-extractor.d.ts +52 -0
  129. package/dist/extraction/liquid-extractor.d.ts.map +1 -0
  130. package/dist/extraction/liquid-extractor.js +313 -0
  131. package/dist/extraction/liquid-extractor.js.map +1 -0
  132. package/dist/extraction/parse-worker.d.ts +8 -0
  133. package/dist/extraction/parse-worker.d.ts.map +1 -0
  134. package/dist/extraction/parse-worker.js +94 -0
  135. package/dist/extraction/parse-worker.js.map +1 -0
  136. package/dist/extraction/svelte-extractor.d.ts +56 -0
  137. package/dist/extraction/svelte-extractor.d.ts.map +1 -0
  138. package/dist/extraction/svelte-extractor.js +272 -0
  139. package/dist/extraction/svelte-extractor.js.map +1 -0
  140. package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
  141. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
  142. package/dist/extraction/tree-sitter-helpers.js +103 -0
  143. package/dist/extraction/tree-sitter-helpers.js.map +1 -0
  144. package/dist/extraction/tree-sitter-types.d.ts +179 -0
  145. package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
  146. package/dist/extraction/tree-sitter-types.js +10 -0
  147. package/dist/extraction/tree-sitter-types.js.map +1 -0
  148. package/dist/extraction/tree-sitter.d.ts +233 -0
  149. package/dist/extraction/tree-sitter.d.ts.map +1 -0
  150. package/dist/extraction/tree-sitter.js +2393 -0
  151. package/dist/extraction/tree-sitter.js.map +1 -0
  152. package/dist/extraction/vue-extractor.d.ts +36 -0
  153. package/dist/extraction/vue-extractor.d.ts.map +1 -0
  154. package/dist/extraction/vue-extractor.js +163 -0
  155. package/dist/extraction/vue-extractor.js.map +1 -0
  156. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  157. package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  158. package/dist/graph/index.d.ts +8 -0
  159. package/dist/graph/index.d.ts.map +1 -0
  160. package/dist/graph/index.js +13 -0
  161. package/dist/graph/index.js.map +1 -0
  162. package/dist/graph/queries.d.ts +106 -0
  163. package/dist/graph/queries.d.ts.map +1 -0
  164. package/dist/graph/queries.js +366 -0
  165. package/dist/graph/queries.js.map +1 -0
  166. package/dist/graph/traversal.d.ts +127 -0
  167. package/dist/graph/traversal.d.ts.map +1 -0
  168. package/dist/graph/traversal.js +493 -0
  169. package/dist/graph/traversal.js.map +1 -0
  170. package/dist/index.d.ts +504 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +982 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/installer/claude-md-template.d.ts +10 -0
  175. package/dist/installer/claude-md-template.d.ts.map +1 -0
  176. package/dist/installer/claude-md-template.js +46 -0
  177. package/dist/installer/claude-md-template.js.map +1 -0
  178. package/dist/installer/config-writer.d.ts +36 -0
  179. package/dist/installer/config-writer.d.ts.map +1 -0
  180. package/dist/installer/config-writer.js +304 -0
  181. package/dist/installer/config-writer.js.map +1 -0
  182. package/dist/installer/index.d.ts +12 -0
  183. package/dist/installer/index.d.ts.map +1 -0
  184. package/dist/installer/index.js +201 -0
  185. package/dist/installer/index.js.map +1 -0
  186. package/dist/mcp/index.d.ts +82 -0
  187. package/dist/mcp/index.d.ts.map +1 -0
  188. package/dist/mcp/index.js +330 -0
  189. package/dist/mcp/index.js.map +1 -0
  190. package/dist/mcp/server-instructions.d.ts +19 -0
  191. package/dist/mcp/server-instructions.d.ts.map +1 -0
  192. package/dist/mcp/server-instructions.js +59 -0
  193. package/dist/mcp/server-instructions.js.map +1 -0
  194. package/dist/mcp/tools.d.ts +187 -0
  195. package/dist/mcp/tools.d.ts.map +1 -0
  196. package/dist/mcp/tools.js +1250 -0
  197. package/dist/mcp/tools.js.map +1 -0
  198. package/dist/mcp/transport.d.ts +89 -0
  199. package/dist/mcp/transport.d.ts.map +1 -0
  200. package/dist/mcp/transport.js +170 -0
  201. package/dist/mcp/transport.js.map +1 -0
  202. package/dist/resolution/frameworks/cargo-workspace.d.ts +18 -0
  203. package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -0
  204. package/dist/resolution/frameworks/cargo-workspace.js +225 -0
  205. package/dist/resolution/frameworks/cargo-workspace.js.map +1 -0
  206. package/dist/resolution/frameworks/csharp.d.ts +8 -0
  207. package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
  208. package/dist/resolution/frameworks/csharp.js +213 -0
  209. package/dist/resolution/frameworks/csharp.js.map +1 -0
  210. package/dist/resolution/frameworks/express.d.ts +8 -0
  211. package/dist/resolution/frameworks/express.d.ts.map +1 -0
  212. package/dist/resolution/frameworks/express.js +225 -0
  213. package/dist/resolution/frameworks/express.js.map +1 -0
  214. package/dist/resolution/frameworks/go.d.ts +8 -0
  215. package/dist/resolution/frameworks/go.d.ts.map +1 -0
  216. package/dist/resolution/frameworks/go.js +158 -0
  217. package/dist/resolution/frameworks/go.js.map +1 -0
  218. package/dist/resolution/frameworks/index.d.ts +41 -0
  219. package/dist/resolution/frameworks/index.d.ts.map +1 -0
  220. package/dist/resolution/frameworks/index.js +129 -0
  221. package/dist/resolution/frameworks/index.js.map +1 -0
  222. package/dist/resolution/frameworks/java.d.ts +8 -0
  223. package/dist/resolution/frameworks/java.d.ts.map +1 -0
  224. package/dist/resolution/frameworks/java.js +177 -0
  225. package/dist/resolution/frameworks/java.js.map +1 -0
  226. package/dist/resolution/frameworks/laravel.d.ts +13 -0
  227. package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
  228. package/dist/resolution/frameworks/laravel.js +248 -0
  229. package/dist/resolution/frameworks/laravel.js.map +1 -0
  230. package/dist/resolution/frameworks/python.d.ts +10 -0
  231. package/dist/resolution/frameworks/python.d.ts.map +1 -0
  232. package/dist/resolution/frameworks/python.js +278 -0
  233. package/dist/resolution/frameworks/python.js.map +1 -0
  234. package/dist/resolution/frameworks/react.d.ts +8 -0
  235. package/dist/resolution/frameworks/react.d.ts.map +1 -0
  236. package/dist/resolution/frameworks/react.js +272 -0
  237. package/dist/resolution/frameworks/react.js.map +1 -0
  238. package/dist/resolution/frameworks/ruby.d.ts +8 -0
  239. package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
  240. package/dist/resolution/frameworks/ruby.js +198 -0
  241. package/dist/resolution/frameworks/ruby.js.map +1 -0
  242. package/dist/resolution/frameworks/rust.d.ts +8 -0
  243. package/dist/resolution/frameworks/rust.d.ts.map +1 -0
  244. package/dist/resolution/frameworks/rust.js +207 -0
  245. package/dist/resolution/frameworks/rust.js.map +1 -0
  246. package/dist/resolution/frameworks/svelte.d.ts +9 -0
  247. package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
  248. package/dist/resolution/frameworks/svelte.js +249 -0
  249. package/dist/resolution/frameworks/svelte.js.map +1 -0
  250. package/dist/resolution/frameworks/swift.d.ts +10 -0
  251. package/dist/resolution/frameworks/swift.d.ts.map +1 -0
  252. package/dist/resolution/frameworks/swift.js +376 -0
  253. package/dist/resolution/frameworks/swift.js.map +1 -0
  254. package/dist/resolution/frameworks/vue.d.ts +9 -0
  255. package/dist/resolution/frameworks/vue.d.ts.map +1 -0
  256. package/dist/resolution/frameworks/vue.js +306 -0
  257. package/dist/resolution/frameworks/vue.js.map +1 -0
  258. package/dist/resolution/import-resolver.d.ts +40 -0
  259. package/dist/resolution/import-resolver.d.ts.map +1 -0
  260. package/dist/resolution/import-resolver.js +663 -0
  261. package/dist/resolution/import-resolver.js.map +1 -0
  262. package/dist/resolution/index.d.ts +106 -0
  263. package/dist/resolution/index.d.ts.map +1 -0
  264. package/dist/resolution/index.js +709 -0
  265. package/dist/resolution/index.js.map +1 -0
  266. package/dist/resolution/name-matcher.d.ts +32 -0
  267. package/dist/resolution/name-matcher.d.ts.map +1 -0
  268. package/dist/resolution/name-matcher.js +384 -0
  269. package/dist/resolution/name-matcher.js.map +1 -0
  270. package/dist/resolution/path-aliases.d.ts +68 -0
  271. package/dist/resolution/path-aliases.d.ts.map +1 -0
  272. package/dist/resolution/path-aliases.js +238 -0
  273. package/dist/resolution/path-aliases.js.map +1 -0
  274. package/dist/resolution/strip-comments.d.ts +27 -0
  275. package/dist/resolution/strip-comments.d.ts.map +1 -0
  276. package/dist/resolution/strip-comments.js +441 -0
  277. package/dist/resolution/strip-comments.js.map +1 -0
  278. package/dist/resolution/types.d.ts +172 -0
  279. package/dist/resolution/types.d.ts.map +1 -0
  280. package/dist/resolution/types.js +8 -0
  281. package/dist/resolution/types.js.map +1 -0
  282. package/dist/search/query-parser.d.ts +57 -0
  283. package/dist/search/query-parser.d.ts.map +1 -0
  284. package/dist/search/query-parser.js +177 -0
  285. package/dist/search/query-parser.js.map +1 -0
  286. package/dist/search/query-utils.d.ts +53 -0
  287. package/dist/search/query-utils.d.ts.map +1 -0
  288. package/dist/search/query-utils.js +347 -0
  289. package/dist/search/query-utils.js.map +1 -0
  290. package/dist/sync/index.d.ts +13 -0
  291. package/dist/sync/index.d.ts.map +1 -0
  292. package/dist/sync/index.js +17 -0
  293. package/dist/sync/index.js.map +1 -0
  294. package/dist/sync/watcher.d.ts +81 -0
  295. package/dist/sync/watcher.d.ts.map +1 -0
  296. package/dist/sync/watcher.js +184 -0
  297. package/dist/sync/watcher.js.map +1 -0
  298. package/dist/types.d.ts +438 -0
  299. package/dist/types.d.ts.map +1 -0
  300. package/dist/types.js +263 -0
  301. package/dist/types.js.map +1 -0
  302. package/dist/ui/shimmer-progress.d.ts +11 -0
  303. package/dist/ui/shimmer-progress.d.ts.map +1 -0
  304. package/dist/ui/shimmer-progress.js +90 -0
  305. package/dist/ui/shimmer-progress.js.map +1 -0
  306. package/dist/ui/shimmer-worker.d.ts +2 -0
  307. package/dist/ui/shimmer-worker.d.ts.map +1 -0
  308. package/dist/ui/shimmer-worker.js +112 -0
  309. package/dist/ui/shimmer-worker.js.map +1 -0
  310. package/dist/ui/types.d.ts +17 -0
  311. package/dist/ui/types.d.ts.map +1 -0
  312. package/dist/ui/types.js +3 -0
  313. package/dist/ui/types.js.map +1 -0
  314. package/dist/utils.d.ts +205 -0
  315. package/dist/utils.d.ts.map +1 -0
  316. package/dist/utils.js +549 -0
  317. package/dist/utils.js.map +1 -0
  318. package/dist/vectors/embedder.d.ts +121 -0
  319. package/dist/vectors/embedder.d.ts.map +1 -0
  320. package/dist/vectors/embedder.js +237 -0
  321. package/dist/vectors/embedder.js.map +1 -0
  322. package/dist/vectors/index.d.ts +9 -0
  323. package/dist/vectors/index.d.ts.map +1 -0
  324. package/dist/vectors/index.js +20 -0
  325. package/dist/vectors/index.js.map +1 -0
  326. package/dist/vectors/manager.d.ts +124 -0
  327. package/dist/vectors/manager.d.ts.map +1 -0
  328. package/dist/vectors/manager.js +322 -0
  329. package/dist/vectors/manager.js.map +1 -0
  330. package/dist/vectors/search.d.ts +143 -0
  331. package/dist/vectors/search.d.ts.map +1 -0
  332. package/dist/vectors/search.js +460 -0
  333. package/dist/vectors/search.js.map +1 -0
  334. package/dist/visualizer/public/index.html +1994 -0
  335. package/dist/visualizer/server.d.ts +46 -0
  336. package/dist/visualizer/server.d.ts.map +1 -0
  337. package/dist/visualizer/server.js +491 -0
  338. package/dist/visualizer/server.js.map +1 -0
  339. package/package.json +60 -0
  340. package/scripts/patch-tree-sitter-dart.js +112 -0
@@ -0,0 +1,1075 @@
1
+ "use strict";
2
+ /**
3
+ * Context Builder
4
+ *
5
+ * Builds rich context for tasks by combining semantic search with graph traversal.
6
+ * Outputs structured context ready to inject into Claude.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.formatContextAsJson = exports.formatContextAsMarkdown = exports.ContextBuilder = void 0;
43
+ exports.createContextBuilder = createContextBuilder;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const formatter_1 = require("./formatter");
47
+ const errors_1 = require("../errors");
48
+ const utils_1 = require("../utils");
49
+ const query_utils_1 = require("../search/query-utils");
50
+ /**
51
+ * Extract likely symbol names from a natural language query
52
+ *
53
+ * Identifies potential code symbols using patterns:
54
+ * - CamelCase: UserService, signInWithGoogle
55
+ * - snake_case: user_service, sign_in
56
+ * - SCREAMING_SNAKE: MAX_RETRIES
57
+ * - dot.notation: app.isPackaged (extracts both sides)
58
+ * - Single words that look like identifiers (no spaces, not common English words)
59
+ *
60
+ * @param query - Natural language query
61
+ * @returns Array of potential symbol names
62
+ */
63
+ function extractSymbolsFromQuery(query) {
64
+ const symbols = new Set();
65
+ // Extract CamelCase identifiers (2+ chars, starts with letter)
66
+ const camelCasePattern = /\b([A-Z][a-z]+(?:[A-Z][a-z]*)*|[a-z]+(?:[A-Z][a-z]*)+)\b/g;
67
+ let match;
68
+ while ((match = camelCasePattern.exec(query)) !== null) {
69
+ if (match[1] && match[1].length >= 2) {
70
+ symbols.add(match[1]);
71
+ }
72
+ }
73
+ // Extract snake_case identifiers
74
+ const snakeCasePattern = /\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b/gi;
75
+ while ((match = snakeCasePattern.exec(query)) !== null) {
76
+ if (match[1] && match[1].length >= 3) {
77
+ symbols.add(match[1]);
78
+ }
79
+ }
80
+ // Extract SCREAMING_SNAKE_CASE
81
+ const screamingPattern = /\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
82
+ while ((match = screamingPattern.exec(query)) !== null) {
83
+ if (match[1]) {
84
+ symbols.add(match[1]);
85
+ }
86
+ }
87
+ // Extract ALL_CAPS acronyms (2+ chars, e.g., REST, HTTP, LRU, API)
88
+ const acronymPattern = /\b([A-Z]{2,})\b/g;
89
+ while ((match = acronymPattern.exec(query)) !== null) {
90
+ if (match[1]) {
91
+ symbols.add(match[1]);
92
+ }
93
+ }
94
+ // Extract dot.notation and split into parts (e.g., "app.isPackaged" -> ["app", "isPackaged"])
95
+ const dotPattern = /\b([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*)+)\b/g;
96
+ while ((match = dotPattern.exec(query)) !== null) {
97
+ if (match[1]) {
98
+ // Add both the full path and individual parts
99
+ symbols.add(match[1]);
100
+ const parts = match[1].split('.');
101
+ for (const part of parts) {
102
+ if (part.length >= 2) {
103
+ symbols.add(part);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ // Extract plain lowercase identifiers (3+ chars, not already matched)
109
+ // Catches symbol names like "undo", "redo", "history", "render", "parse"
110
+ const lowercasePattern = /\b([a-z][a-z0-9]{2,})\b/g;
111
+ while ((match = lowercasePattern.exec(query)) !== null) {
112
+ if (match[1]) {
113
+ symbols.add(match[1]);
114
+ }
115
+ }
116
+ // Filter out common English words that aren't likely symbol names
117
+ const commonWords = new Set([
118
+ 'the', 'and', 'for', 'with', 'from', 'this', 'that', 'have', 'been',
119
+ 'will', 'would', 'could', 'should', 'does', 'done', 'make', 'made',
120
+ 'use', 'used', 'using', 'work', 'works', 'find', 'found', 'show',
121
+ 'call', 'called', 'calling', 'get', 'set', 'add', 'all', 'any',
122
+ 'how', 'what', 'when', 'where', 'which', 'who', 'why',
123
+ 'not', 'but', 'are', 'was', 'were', 'has', 'had', 'its',
124
+ 'can', 'did', 'may', 'also', 'into', 'than', 'then', 'them',
125
+ 'each', 'other', 'some', 'such', 'only', 'same', 'about',
126
+ 'after', 'before', 'between', 'through', 'during', 'without',
127
+ 'again', 'further', 'once', 'here', 'there', 'both', 'just',
128
+ 'more', 'most', 'very', 'being', 'having', 'doing',
129
+ 'system', 'need', 'needs', 'want', 'wants', 'like', 'look',
130
+ 'change', 'changes', 'changed', 'changing',
131
+ // Common English nouns/verbs that match thousands of unrelated code symbols
132
+ 'layer', 'handle', 'handles', 'handling', 'incoming', 'outgoing',
133
+ 'data', 'flow', 'flows', 'level', 'levels', 'request', 'requests',
134
+ 'response', 'responses', 'implement', 'implements', 'implementation',
135
+ 'interface', 'interfaces', 'class', 'classes', 'method', 'methods',
136
+ 'trigger', 'triggers', 'affected', 'affect', 'affects',
137
+ 'else', 'code', 'failing', 'failed', 'silently', 'decide', 'decides',
138
+ 'return', 'returns', 'returned', 'take', 'takes', 'taken',
139
+ 'check', 'checks', 'checked', 'create', 'creates', 'created',
140
+ 'read', 'reads', 'write', 'writes', 'written',
141
+ 'start', 'starts', 'stop', 'stops', 'run', 'runs', 'running',
142
+ ]);
143
+ return Array.from(symbols).filter(s => !commonWords.has(s.toLowerCase()));
144
+ }
145
+ /**
146
+ * Default options for context building
147
+ *
148
+ * Tuned for minimal context usage while still providing useful results:
149
+ * - Fewer nodes and code blocks by default
150
+ * - Smaller code block size limit
151
+ * - Shallower traversal
152
+ */
153
+ const DEFAULT_BUILD_OPTIONS = {
154
+ maxNodes: 20, // Reduced from 50 - most tasks don't need 50 symbols
155
+ maxCodeBlocks: 5, // Reduced from 10 - only show most relevant code
156
+ maxCodeBlockSize: 1500, // Reduced from 2000
157
+ includeCode: true,
158
+ format: 'markdown',
159
+ searchLimit: 3, // Reduced from 5 - fewer entry points
160
+ traversalDepth: 1, // Reduced from 2 - shallower graph expansion
161
+ minScore: 0.3,
162
+ };
163
+ /**
164
+ * Node kinds that provide high information value in context results.
165
+ * Imports/exports are excluded because they have near-zero information density -
166
+ * they tell you something exists, not how it works.
167
+ */
168
+ const HIGH_VALUE_NODE_KINDS = [
169
+ 'function', 'method', 'class', 'interface', 'type_alias', 'struct', 'trait',
170
+ 'component', 'route', 'variable', 'constant', 'enum', 'module', 'namespace',
171
+ ];
172
+ /**
173
+ * Default options for finding relevant context
174
+ */
175
+ const DEFAULT_FIND_OPTIONS = {
176
+ searchLimit: 3, // Reduced from 5
177
+ traversalDepth: 1, // Reduced from 2
178
+ maxNodes: 20, // Reduced from 50
179
+ minScore: 0.3,
180
+ edgeKinds: [],
181
+ nodeKinds: HIGH_VALUE_NODE_KINDS, // Filter out imports/exports by default
182
+ };
183
+ /**
184
+ * Context Builder
185
+ *
186
+ * Coordinates semantic search and graph traversal to build
187
+ * comprehensive context for tasks.
188
+ */
189
+ class ContextBuilder {
190
+ projectRoot;
191
+ queries;
192
+ traverser;
193
+ vectorManager;
194
+ constructor(projectRoot, queries, traverser, vectorManager) {
195
+ this.projectRoot = projectRoot;
196
+ this.queries = queries;
197
+ this.traverser = traverser;
198
+ this.vectorManager = vectorManager;
199
+ }
200
+ /**
201
+ * Build context for a task
202
+ *
203
+ * Pipeline:
204
+ * 1. Parse task input (string or {title, description})
205
+ * 2. Run semantic search to find entry points
206
+ * 3. Expand graph around entry points
207
+ * 4. Extract code blocks for key nodes
208
+ * 5. Format output for Claude
209
+ *
210
+ * @param input - Task description or object with title/description
211
+ * @param options - Build options
212
+ * @returns TaskContext (structured) or formatted string
213
+ */
214
+ async buildContext(input, options = {}) {
215
+ const opts = { ...DEFAULT_BUILD_OPTIONS, ...options };
216
+ // Parse input
217
+ const query = typeof input === 'string' ? input : `${input.title}${input.description ? `: ${input.description}` : ''}`;
218
+ // Find relevant context (semantic search + graph expansion)
219
+ const subgraph = await this.findRelevantContext(query, {
220
+ searchLimit: opts.searchLimit,
221
+ traversalDepth: opts.traversalDepth,
222
+ maxNodes: opts.maxNodes,
223
+ minScore: opts.minScore,
224
+ });
225
+ // Get entry points (nodes from semantic search)
226
+ const entryPoints = this.getEntryPoints(subgraph);
227
+ // Extract code blocks for key nodes
228
+ const codeBlocks = opts.includeCode
229
+ ? await this.extractCodeBlocks(subgraph, opts.maxCodeBlocks, opts.maxCodeBlockSize)
230
+ : [];
231
+ // Get related files
232
+ const relatedFiles = this.getRelatedFiles(subgraph);
233
+ // Generate summary
234
+ const summary = this.generateSummary(query, subgraph, entryPoints);
235
+ // Calculate stats
236
+ const stats = {
237
+ nodeCount: subgraph.nodes.size,
238
+ edgeCount: subgraph.edges.length,
239
+ fileCount: relatedFiles.length,
240
+ codeBlockCount: codeBlocks.length,
241
+ totalCodeSize: codeBlocks.reduce((sum, block) => sum + block.content.length, 0),
242
+ };
243
+ const context = {
244
+ query,
245
+ subgraph,
246
+ entryPoints,
247
+ codeBlocks,
248
+ relatedFiles,
249
+ summary,
250
+ stats,
251
+ };
252
+ // Return formatted output or raw context
253
+ if (opts.format === 'markdown') {
254
+ return (0, formatter_1.formatContextAsMarkdown)(context);
255
+ }
256
+ else if (opts.format === 'json') {
257
+ return (0, formatter_1.formatContextAsJson)(context);
258
+ }
259
+ return context;
260
+ }
261
+ /**
262
+ * Find relevant subgraph for a query
263
+ *
264
+ * Uses hybrid search combining exact symbol lookup with semantic search:
265
+ * 1. Extract potential symbol names from query
266
+ * 2. Look up exact matches for those symbols (high confidence)
267
+ * 3. Use semantic search for concept matching
268
+ * 4. Merge results, prioritizing exact matches
269
+ * 5. Traverse graph from entry points
270
+ *
271
+ * @param query - Natural language query
272
+ * @param options - Search and traversal options
273
+ * @returns Subgraph of relevant nodes and edges
274
+ */
275
+ async findRelevantContext(query, options = {}) {
276
+ const opts = { ...DEFAULT_FIND_OPTIONS, ...options };
277
+ // Start with empty subgraph
278
+ const nodes = new Map();
279
+ const edges = [];
280
+ const roots = [];
281
+ // Handle empty query - return empty subgraph
282
+ if (!query || query.trim().length === 0) {
283
+ return { nodes, edges, roots };
284
+ }
285
+ // === HYBRID SEARCH ===
286
+ // Step 1: Extract potential symbol names from query
287
+ const symbolsFromQuery = extractSymbolsFromQuery(query);
288
+ (0, errors_1.logDebug)('Extracted symbols from query', { query, symbols: symbolsFromQuery });
289
+ // Step 2: Look up exact matches for extracted symbols
290
+ let exactMatches = [];
291
+ if (symbolsFromQuery.length > 0) {
292
+ try {
293
+ // Get more results so we can apply co-location boosting before trimming
294
+ exactMatches = this.queries.findNodesByExactName(symbolsFromQuery, {
295
+ limit: Math.ceil(opts.searchLimit * 5),
296
+ kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
297
+ });
298
+ // Co-location boost: when multiple extracted symbols appear in the same file,
299
+ // those results are much more likely to be what the user is looking for.
300
+ // E.g., "scrapeLoop" + "run" both in scrape/scrape.go → boost both.
301
+ if (exactMatches.length > 1) {
302
+ // Build a map of files → how many distinct symbol names matched in that file
303
+ const fileSymbolCounts = new Map();
304
+ for (const r of exactMatches) {
305
+ const names = fileSymbolCounts.get(r.node.filePath) || new Set();
306
+ names.add(r.node.name.toLowerCase());
307
+ fileSymbolCounts.set(r.node.filePath, names);
308
+ }
309
+ // Boost results in files where multiple query symbols co-occur
310
+ exactMatches = exactMatches.map(r => {
311
+ const symbolCount = fileSymbolCounts.get(r.node.filePath)?.size || 1;
312
+ return {
313
+ ...r,
314
+ score: symbolCount > 1 ? r.score + (symbolCount - 1) * 20 : r.score,
315
+ };
316
+ });
317
+ exactMatches.sort((a, b) => b.score - a.score);
318
+ }
319
+ // Trim back to reasonable size
320
+ exactMatches = exactMatches.slice(0, Math.ceil(opts.searchLimit * 2));
321
+ (0, errors_1.logDebug)('Exact symbol matches', { count: exactMatches.length });
322
+ }
323
+ catch (error) {
324
+ (0, errors_1.logDebug)('Exact symbol lookup failed', { error: String(error) });
325
+ }
326
+ }
327
+ // Step 2b: Search for extracted symbols as definition (class/interface) prefixes.
328
+ // When the user writes "REST", "bulk", or "allocation", they usually mean classes
329
+ // like RestController, BulkRequest, AllocationService — not nodes named exactly that.
330
+ // Also tries stem variants: "caching" → "cache" finds Cache, CacheBuilder.
331
+ if (symbolsFromQuery.length > 0) {
332
+ const definitionKinds = ['class', 'interface', 'struct', 'trait',
333
+ 'protocol', 'enum', 'type_alias'];
334
+ // Expand symbols with stem variants for broader definition matching
335
+ const expandedSymbols = new Set(symbolsFromQuery);
336
+ for (const sym of symbolsFromQuery) {
337
+ for (const variant of (0, query_utils_1.getStemVariants)(sym)) {
338
+ expandedSymbols.add(variant);
339
+ }
340
+ }
341
+ for (const sym of expandedSymbols) {
342
+ // Title-case the symbol: "REST" → "Rest", "bulk" → "Bulk", "allocation" → "Allocation"
343
+ const titleCased = sym.charAt(0).toUpperCase() + sym.slice(1).toLowerCase();
344
+ if (titleCased === sym)
345
+ continue; // already title-case (e.g., "Engine") — handled by exact match
346
+ // Fetch more results since popular prefixes have many matches
347
+ const prefixResults = this.queries.searchNodes(titleCased, {
348
+ limit: 30,
349
+ kinds: definitionKinds,
350
+ });
351
+ const matched = [];
352
+ for (const r of prefixResults) {
353
+ if (r.node.name.toLowerCase().startsWith(titleCased.toLowerCase())) {
354
+ // Favor shorter names: "AllocationService" (18 chars) over
355
+ // "AllocationBalancingRoundMetrics" (31 chars). Core classes tend
356
+ // to have concise names; test/helper classes are verbose.
357
+ const brevityBonus = Math.max(0, 10 - (r.node.name.length - titleCased.length) / 3);
358
+ matched.push({ ...r, score: r.score + 15 + brevityBonus });
359
+ }
360
+ }
361
+ matched.sort((a, b) => b.score - a.score);
362
+ for (const r of matched.slice(0, Math.ceil(opts.searchLimit))) {
363
+ const existing = exactMatches.find(e => e.node.id === r.node.id);
364
+ if (!existing) {
365
+ exactMatches.push(r);
366
+ }
367
+ }
368
+ }
369
+ exactMatches.sort((a, b) => b.score - a.score);
370
+ exactMatches = exactMatches.slice(0, Math.ceil(opts.searchLimit * 3));
371
+ }
372
+ // Step 3: Try semantic search if vector manager is available
373
+ let semanticResults = [];
374
+ if (this.vectorManager && this.vectorManager.isInitialized()) {
375
+ try {
376
+ semanticResults = await this.vectorManager.search(query, {
377
+ limit: opts.searchLimit,
378
+ kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
379
+ });
380
+ (0, errors_1.logDebug)('Semantic search results', { count: semanticResults.length });
381
+ }
382
+ catch (error) {
383
+ (0, errors_1.logDebug)('Semantic search failed, falling back to text search', { query, error: String(error) });
384
+ }
385
+ }
386
+ // Step 4: Always run text search for natural language term matching
387
+ // This catches file-name and node-name matches that semantic search may miss,
388
+ // which is critical for template-heavy codebases (e.g., Liquid/Shopify themes)
389
+ // where file names are the primary identifiers.
390
+ let textResults = [];
391
+ try {
392
+ const searchTerms = (0, query_utils_1.extractSearchTerms)(query);
393
+ if (searchTerms.length > 0) {
394
+ // Search each term individually to get broader coverage,
395
+ // then boost results that match multiple terms
396
+ const termResultsMap = new Map();
397
+ // When no explicit kind filter is set, exclude imports — they flood FTS
398
+ // results with qualified name matches (e.g., "REST" matches 445K import paths)
399
+ // but are almost never what exploration queries want.
400
+ const searchKinds = opts.nodeKinds && opts.nodeKinds.length > 0
401
+ ? opts.nodeKinds
402
+ : ['file', 'module', 'class', 'struct', 'interface', 'trait', 'protocol',
403
+ 'function', 'method', 'property', 'field', 'variable', 'constant',
404
+ 'enum', 'enum_member', 'type_alias', 'namespace', 'export',
405
+ 'route', 'component'];
406
+ for (const term of searchTerms) {
407
+ const termResults = this.queries.searchNodes(term, {
408
+ limit: opts.searchLimit * 2,
409
+ kinds: searchKinds,
410
+ });
411
+ for (const r of termResults) {
412
+ const existing = termResultsMap.get(r.node.id);
413
+ if (existing) {
414
+ existing.termHits++;
415
+ existing.result.score = Math.max(existing.result.score, r.score);
416
+ }
417
+ else {
418
+ termResultsMap.set(r.node.id, { result: r, termHits: 1 });
419
+ }
420
+ }
421
+ }
422
+ // Boost results matching multiple terms and sort
423
+ textResults = Array.from(termResultsMap.values())
424
+ .map(({ result, termHits }) => ({
425
+ ...result,
426
+ score: result.score + (termHits - 1) * 5,
427
+ }))
428
+ .sort((a, b) => b.score - a.score)
429
+ .slice(0, opts.searchLimit * 2);
430
+ }
431
+ (0, errors_1.logDebug)('Text search results', { count: textResults.length });
432
+ }
433
+ catch (error) {
434
+ (0, errors_1.logDebug)('Text search failed', { query, error: String(error) });
435
+ }
436
+ // Step 4: Merge results, taking the max score when duplicates appear
437
+ // across search channels. Exact matches may have lower scores than FTS
438
+ // results for the same node — use the best score from any channel.
439
+ const resultById = new Map();
440
+ let searchResults = [];
441
+ // Add exact matches first
442
+ for (const result of exactMatches) {
443
+ const existing = resultById.get(result.node.id);
444
+ if (existing) {
445
+ existing.score = Math.max(existing.score, result.score);
446
+ }
447
+ else {
448
+ resultById.set(result.node.id, result);
449
+ searchResults.push(result);
450
+ }
451
+ }
452
+ // Add text search results, upgrading scores for duplicates
453
+ for (const result of textResults) {
454
+ const existing = resultById.get(result.node.id);
455
+ if (existing) {
456
+ existing.score = Math.max(existing.score, result.score);
457
+ }
458
+ else {
459
+ resultById.set(result.node.id, result);
460
+ searchResults.push(result);
461
+ }
462
+ }
463
+ // Add semantic results
464
+ for (const result of semanticResults) {
465
+ const existing = resultById.get(result.node.id);
466
+ if (existing) {
467
+ existing.score = Math.max(existing.score, result.score);
468
+ }
469
+ else {
470
+ resultById.set(result.node.id, result);
471
+ searchResults.push(result);
472
+ }
473
+ }
474
+ const queryLower = query.toLowerCase();
475
+ const isTestQuery = queryLower.includes('test') || queryLower.includes('spec');
476
+ // Deprioritize test files early so they don't take multi-term boost slots
477
+ if (!isTestQuery) {
478
+ for (const result of searchResults) {
479
+ if ((0, query_utils_1.isTestFile)(result.node.filePath)) {
480
+ result.score *= 0.3;
481
+ }
482
+ }
483
+ }
484
+ // Step 5a: Multi-term co-occurrence re-ranking (applied BEFORE truncation).
485
+ // For multi-word queries like "search execution from request to shard",
486
+ // nodes matching 2+ query terms in their name or path are far more relevant
487
+ // than nodes matching just one generic term. Without this, "ExecutionUtils"
488
+ // (matches only "execution") fills budget slots meant for "ShardSearchRequest"
489
+ // (matches "shard" + "search" + "request").
490
+ const queryTermsForBoost = (0, query_utils_1.extractSearchTerms)(query);
491
+ if (queryTermsForBoost.length >= 2) {
492
+ // Group terms that are substrings of each other (stem variants of the same
493
+ // root word). "indexed", "indexe", "index" should count as ONE concept match,
494
+ // not three. Without this, stem variants inflate matchCount and give false
495
+ // multi-term boosts to symbols matching one root word multiple times.
496
+ const termGroups = [];
497
+ const sorted = [...queryTermsForBoost].sort((a, b) => b.length - a.length);
498
+ const assigned = new Set();
499
+ for (const term of sorted) {
500
+ if (assigned.has(term))
501
+ continue;
502
+ const group = [term];
503
+ assigned.add(term);
504
+ for (const other of sorted) {
505
+ if (assigned.has(other))
506
+ continue;
507
+ if (term.includes(other) || other.includes(term)) {
508
+ group.push(other);
509
+ assigned.add(other);
510
+ }
511
+ }
512
+ termGroups.push(group);
513
+ }
514
+ // Build a set of exact-match node IDs so we can exempt them from dampening.
515
+ // When the query is "LiveEditMode DevServerPreview", these are specific
516
+ // symbols the user asked for — dampening them because they only match 1
517
+ // term group is counter-productive.
518
+ const exactMatchIds = new Set(exactMatches.map(r => r.node.id));
519
+ for (const result of searchResults) {
520
+ // Check term matches in name (substring) and path DIRECTORIES (exact).
521
+ // Directory segments must match exactly — "search" matches directory
522
+ // "search/" but NOT "elasticsearch/". The class name is checked
523
+ // separately via substring match on the node name.
524
+ const nameLower = result.node.name.toLowerCase();
525
+ const dirSegments = path.dirname(result.node.filePath).toLowerCase().split('/');
526
+ let matchCount = 0;
527
+ for (const group of termGroups) {
528
+ const groupMatches = group.some(term => {
529
+ const inName = nameLower.includes(term);
530
+ const inDir = dirSegments.some(seg => seg === term);
531
+ return inName || inDir;
532
+ });
533
+ if (groupMatches)
534
+ matchCount++;
535
+ }
536
+ if (matchCount >= 2) {
537
+ // Multiplicative boost — 2 terms → 2x, 3 terms → 2.5x
538
+ result.score *= 1 + matchCount * 0.5;
539
+ }
540
+ else if (!exactMatchIds.has(result.node.id)) {
541
+ // Mild dampen for single-term matches — they might be generic
542
+ // but could also be the right result (e.g., "Protocol" class for an IPC query).
543
+ // Exempt exact name matches: they are specific symbols the user queried for.
544
+ result.score *= 0.6;
545
+ }
546
+ }
547
+ searchResults.sort((a, b) => b.score - a.score);
548
+ }
549
+ // Step 5b: CamelCase-boundary matching via LIKE query.
550
+ // FTS can't find "Search" inside "TransportSearchAction" (one FTS token).
551
+ // LIKE reliably finds these substring matches. Results are appended with
552
+ // guaranteed slots so they don't compete with higher-scoring prefix matches.
553
+ if (symbolsFromQuery.length > 0) {
554
+ const camelDefinitionKinds = ['class', 'interface', 'struct', 'trait',
555
+ 'protocol', 'enum', 'type_alias'];
556
+ const camelSearchedTerms = new Set();
557
+ const searchIdSet = new Set(searchResults.map(r => r.node.id));
558
+ // Track per-node term hits for multi-term boosting
559
+ const camelNodeTerms = new Map();
560
+ const maxCamelPerTerm = Math.ceil(opts.searchLimit / 2);
561
+ for (const sym of symbolsFromQuery) {
562
+ const titleCased = sym.charAt(0).toUpperCase() + sym.slice(1).toLowerCase();
563
+ if (titleCased.length < 3)
564
+ continue;
565
+ const termKey = titleCased.toLowerCase();
566
+ if (camelSearchedTerms.has(termKey))
567
+ continue;
568
+ camelSearchedTerms.add(termKey);
569
+ // Fetch a large batch — popular terms like "Search" in Elasticsearch
570
+ // have hundreds of substring matches. The LIKE scan cost is the same
571
+ // regardless of LIMIT (SQLite scans all matches to sort), so we fetch
572
+ // generously and let path-relevance scoring pick the best ones.
573
+ const likeResults = this.queries.findNodesByNameSubstring(titleCased, {
574
+ limit: 200,
575
+ kinds: camelDefinitionKinds,
576
+ excludePrefix: true,
577
+ });
578
+ // Filter to CamelCase boundaries, score by path relevance, and take top N
579
+ const termCandidates = [];
580
+ for (const r of likeResults) {
581
+ const name = r.node.name;
582
+ const idx = name.indexOf(titleCased);
583
+ if (idx <= 0)
584
+ continue;
585
+ // Accept CamelCase boundary (lowercase before match) OR
586
+ // acronym boundary (uppercase before match, e.g., RPCProtocol)
587
+ if (!/[a-zA-Z]/.test(name.charAt(idx - 1)))
588
+ continue;
589
+ if (searchIdSet.has(r.node.id))
590
+ continue;
591
+ if ((0, query_utils_1.isTestFile)(r.node.filePath) && !isTestQuery)
592
+ continue;
593
+ const pathScore = (0, query_utils_1.scorePathRelevance)(r.node.filePath, query);
594
+ const brevityBonus = Math.max(0, 6 - (name.length - titleCased.length) / 4);
595
+ termCandidates.push({ node: r.node, score: 8 + brevityBonus + pathScore });
596
+ }
597
+ termCandidates.sort((a, b) => b.score - a.score);
598
+ // Widen the per-term pool for accumulation so multi-term co-occurrences
599
+ // can be discovered. A class matching 3 query terms at CamelCase boundaries
600
+ // is far more relevant than one matching just 1, but it needs to survive
601
+ // the per-term cut for EACH term to accumulate its count.
602
+ const accumPerTerm = maxCamelPerTerm * 4;
603
+ for (const r of termCandidates.slice(0, accumPerTerm)) {
604
+ const existing = camelNodeTerms.get(r.node.id);
605
+ if (existing) {
606
+ existing.termCount++;
607
+ }
608
+ else {
609
+ camelNodeTerms.set(r.node.id, {
610
+ result: r,
611
+ termCount: 1,
612
+ });
613
+ }
614
+ }
615
+ }
616
+ // Append CamelCase matches with multi-term boost.
617
+ // These are structurally important (class names containing query terms at
618
+ // CamelCase boundaries) but score much lower than FTS results. Scale their
619
+ // scores up so multi-term CamelCase matches can compete with FTS results.
620
+ const camelResults = [];
621
+ for (const [, info] of camelNodeTerms) {
622
+ // Multi-term CamelCase matches are extremely relevant — a class matching
623
+ // 3+ query terms in its name (e.g., ExtensionHostProcess) is almost
624
+ // certainly what the user wants. Scale aggressively.
625
+ info.result.score = info.result.score * (1 + info.termCount) + (info.termCount - 1) * 30;
626
+ camelResults.push(info.result);
627
+ }
628
+ camelResults.sort((a, b) => b.score - a.score);
629
+ const maxCamelTotal = opts.searchLimit;
630
+ for (const r of camelResults.slice(0, maxCamelTotal)) {
631
+ searchResults.push(r);
632
+ searchIdSet.add(r.node.id);
633
+ }
634
+ // Step 5c: Compound term matching — find classes whose name contains 2+
635
+ // query terms at ANY position (not just CamelCase boundaries).
636
+ // The CamelCase step above requires idx > 0, which misses classes that
637
+ // START with a query term (e.g., "SearchShardsRequest" starts with "Search").
638
+ // For multi-word queries, a class matching multiple query terms in its name
639
+ // is almost certainly relevant regardless of position.
640
+ if (symbolsFromQuery.length >= 2) {
641
+ // Collect ALL LIKE results per term (reusing findNodesByNameSubstring)
642
+ // but without the CamelCase boundary or prefix exclusion filters.
643
+ const compoundTermMap = new Map();
644
+ for (const sym of symbolsFromQuery) {
645
+ const titleCased = sym.charAt(0).toUpperCase() + sym.slice(1).toLowerCase();
646
+ if (titleCased.length < 3)
647
+ continue;
648
+ const likeResults = this.queries.findNodesByNameSubstring(titleCased, {
649
+ limit: 200,
650
+ kinds: camelDefinitionKinds,
651
+ excludePrefix: false,
652
+ });
653
+ for (const r of likeResults) {
654
+ if (searchIdSet.has(r.node.id))
655
+ continue;
656
+ if ((0, query_utils_1.isTestFile)(r.node.filePath) && !isTestQuery)
657
+ continue;
658
+ const entry = compoundTermMap.get(r.node.id);
659
+ if (entry) {
660
+ entry.terms.add(titleCased);
661
+ }
662
+ else {
663
+ compoundTermMap.set(r.node.id, { node: r.node, terms: new Set([titleCased]) });
664
+ }
665
+ }
666
+ }
667
+ // Keep only nodes matching 2+ distinct terms
668
+ const compoundResults = [];
669
+ for (const [, entry] of compoundTermMap) {
670
+ if (entry.terms.size >= 2) {
671
+ const pathScore = (0, query_utils_1.scorePathRelevance)(entry.node.filePath, query);
672
+ const brevityBonus = Math.max(0, 6 - entry.node.name.length / 8);
673
+ compoundResults.push({
674
+ node: entry.node,
675
+ score: 10 + (entry.terms.size - 1) * 20 + pathScore + brevityBonus,
676
+ });
677
+ }
678
+ }
679
+ compoundResults.sort((a, b) => b.score - a.score);
680
+ const maxCompound = Math.ceil(opts.searchLimit / 2);
681
+ for (const r of compoundResults.slice(0, maxCompound)) {
682
+ searchResults.push(r);
683
+ searchIdSet.add(r.node.id);
684
+ }
685
+ }
686
+ }
687
+ // Final sort and truncation — all search channels (exact, text, CamelCase,
688
+ // compound) have now contributed. Sort by score so multi-term matches from
689
+ // later steps can outrank dampened single-term matches from earlier steps.
690
+ searchResults.sort((a, b) => b.score - a.score);
691
+ searchResults = searchResults.slice(0, opts.searchLimit * 3);
692
+ // Filter by minimum score
693
+ let filteredResults = searchResults.filter((r) => r.score >= opts.minScore);
694
+ // Resolve imports/exports to their actual definitions
695
+ // If someone searches "terminal" and finds `import { TerminalPanel }`,
696
+ // they want the TerminalPanel class, not the import statement
697
+ filteredResults = this.resolveImportsToDefinitions(filteredResults);
698
+ // Cap entry points so traversal budget isn't spread too thin.
699
+ // With 36 entry points and maxNodes=120, each gets only 3 nodes — useless.
700
+ // Cap to searchLimit so each entry point gets a meaningful traversal budget.
701
+ if (filteredResults.length > opts.searchLimit) {
702
+ filteredResults = filteredResults.slice(0, opts.searchLimit);
703
+ }
704
+ // Add entry points to subgraph
705
+ for (const result of filteredResults) {
706
+ nodes.set(result.node.id, result.node);
707
+ roots.push(result.node.id);
708
+ }
709
+ // Expand type hierarchy for class/interface entry points.
710
+ // BFS often exhausts its per-entry-point budget on contained methods
711
+ // before reaching extends/implements neighbors. This dedicated step
712
+ // ensures subclasses and superclasses always appear in results.
713
+ // Budget: up to maxNodes/4 hierarchy nodes to avoid flooding.
714
+ const typeHierarchyKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol']);
715
+ const maxHierarchyNodes = Math.ceil(opts.maxNodes / 4);
716
+ let hierarchyNodesAdded = 0;
717
+ for (const result of filteredResults) {
718
+ if (hierarchyNodesAdded >= maxHierarchyNodes)
719
+ break;
720
+ if (typeHierarchyKinds.has(result.node.kind)) {
721
+ const hierarchy = this.traverser.getTypeHierarchy(result.node.id);
722
+ for (const [id, node] of hierarchy.nodes) {
723
+ if (!nodes.has(id)) {
724
+ nodes.set(id, node);
725
+ hierarchyNodesAdded++;
726
+ }
727
+ }
728
+ for (const edge of hierarchy.edges) {
729
+ const exists = edges.some((e) => e.source === edge.source && e.target === edge.target && e.kind === edge.kind);
730
+ if (!exists) {
731
+ edges.push(edge);
732
+ }
733
+ }
734
+ }
735
+ }
736
+ // Pass 2: expand hierarchy of newly-discovered parent types to find siblings.
737
+ // E.g., InternalEngine → Engine (parent, from pass 1) → ReadOnlyEngine (sibling).
738
+ if (hierarchyNodesAdded > 0) {
739
+ const pass2Candidates = [...nodes.values()].filter(n => typeHierarchyKinds.has(n.kind) && !roots.includes(n.id));
740
+ for (const candidate of pass2Candidates) {
741
+ if (hierarchyNodesAdded >= maxHierarchyNodes)
742
+ break;
743
+ const siblingHierarchy = this.traverser.getTypeHierarchy(candidate.id);
744
+ for (const [id, node] of siblingHierarchy.nodes) {
745
+ if (!nodes.has(id) && hierarchyNodesAdded < maxHierarchyNodes) {
746
+ nodes.set(id, node);
747
+ hierarchyNodesAdded++;
748
+ }
749
+ }
750
+ for (const edge of siblingHierarchy.edges) {
751
+ if (nodes.has(edge.source) && nodes.has(edge.target)) {
752
+ const exists = edges.some((e) => e.source === edge.source && e.target === edge.target && e.kind === edge.kind);
753
+ if (!exists) {
754
+ edges.push(edge);
755
+ }
756
+ }
757
+ }
758
+ }
759
+ }
760
+ // Traverse from each entry point
761
+ for (const result of filteredResults) {
762
+ const traversalResult = this.traverser.traverseBFS(result.node.id, {
763
+ maxDepth: opts.traversalDepth,
764
+ edgeKinds: opts.edgeKinds && opts.edgeKinds.length > 0 ? opts.edgeKinds : undefined,
765
+ nodeKinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
766
+ direction: 'both',
767
+ limit: Math.ceil(opts.maxNodes / Math.max(1, filteredResults.length)),
768
+ });
769
+ // Merge nodes
770
+ for (const [id, node] of traversalResult.nodes) {
771
+ if (!nodes.has(id)) {
772
+ nodes.set(id, node);
773
+ }
774
+ }
775
+ // Merge edges (avoid duplicates)
776
+ for (const edge of traversalResult.edges) {
777
+ const exists = edges.some((e) => e.source === edge.source && e.target === edge.target && e.kind === edge.kind);
778
+ if (!exists) {
779
+ edges.push(edge);
780
+ }
781
+ }
782
+ }
783
+ // Trim to max nodes if needed
784
+ let finalNodes = nodes;
785
+ let finalEdges = edges;
786
+ if (nodes.size > opts.maxNodes) {
787
+ // Prioritize entry points and their direct neighbors
788
+ const priorityIds = new Set(roots);
789
+ for (const edge of edges) {
790
+ if (priorityIds.has(edge.source)) {
791
+ priorityIds.add(edge.target);
792
+ }
793
+ if (priorityIds.has(edge.target)) {
794
+ priorityIds.add(edge.source);
795
+ }
796
+ }
797
+ // Keep priority nodes, then fill remaining slots
798
+ finalNodes = new Map();
799
+ for (const id of priorityIds) {
800
+ const node = nodes.get(id);
801
+ if (node && finalNodes.size < opts.maxNodes) {
802
+ finalNodes.set(id, node);
803
+ }
804
+ }
805
+ // Fill remaining from other nodes
806
+ for (const [id, node] of nodes) {
807
+ if (finalNodes.size >= opts.maxNodes)
808
+ break;
809
+ if (!finalNodes.has(id)) {
810
+ finalNodes.set(id, node);
811
+ }
812
+ }
813
+ // Filter edges to only include kept nodes
814
+ finalEdges = edges.filter((e) => finalNodes.has(e.source) && finalNodes.has(e.target));
815
+ }
816
+ // Per-file diversity cap: prevent any single file from monopolizing the
817
+ // node budget. When BFS traverses from a method, it follows `contains`
818
+ // to the parent class, then back down to all sibling methods. With
819
+ // multiple entry points in the same class, one file can consume 30-40%
820
+ // of maxNodes. Cap each file to ~20% to ensure cross-file diversity.
821
+ const maxPerFile = Math.max(5, Math.ceil(opts.maxNodes * 0.2));
822
+ const fileCounts = new Map();
823
+ for (const [id, node] of finalNodes) {
824
+ const ids = fileCounts.get(node.filePath) || [];
825
+ ids.push(id);
826
+ fileCounts.set(node.filePath, ids);
827
+ }
828
+ const rootSet = new Set(roots);
829
+ for (const [, nodeIds] of fileCounts) {
830
+ if (nodeIds.length <= maxPerFile)
831
+ continue;
832
+ // Sort: entry points first, then classes/interfaces, then others
833
+ const kindPriority = {
834
+ class: 3, interface: 3, struct: 3, trait: 3, protocol: 3, enum: 3,
835
+ method: 1, function: 1, property: 0, field: 0, variable: 0,
836
+ };
837
+ nodeIds.sort((a, b) => {
838
+ const aRoot = rootSet.has(a) ? 10 : 0;
839
+ const bRoot = rootSet.has(b) ? 10 : 0;
840
+ const aKind = kindPriority[finalNodes.get(a).kind] ?? 0;
841
+ const bKind = kindPriority[finalNodes.get(b).kind] ?? 0;
842
+ return (bRoot + bKind) - (aRoot + aKind);
843
+ });
844
+ // Remove excess nodes (keep the highest-priority ones)
845
+ for (const id of nodeIds.slice(maxPerFile)) {
846
+ finalNodes.delete(id);
847
+ }
848
+ }
849
+ // Non-production node cap: limit test/sample/integration/example files to
850
+ // at most 15% of the budget. Many codebases have dozens of near-identical
851
+ // test implementations (e.g., 6 Guard classes in integration tests) that
852
+ // individually survive score dampening but collectively flood the result.
853
+ // Test entry points are NOT exempt — they should be evicted too.
854
+ if (!isTestQuery) {
855
+ const maxNonProd = Math.max(3, Math.ceil(opts.maxNodes * 0.15));
856
+ const nonProdIds = [];
857
+ for (const [id, node] of finalNodes) {
858
+ if ((0, query_utils_1.isTestFile)(node.filePath)) {
859
+ nonProdIds.push(id);
860
+ }
861
+ }
862
+ if (nonProdIds.length > maxNonProd) {
863
+ for (const id of nonProdIds.slice(maxNonProd)) {
864
+ finalNodes.delete(id);
865
+ // Also remove from roots — test file entry points shouldn't anchor results
866
+ const rootIdx = roots.indexOf(id);
867
+ if (rootIdx !== -1)
868
+ roots.splice(rootIdx, 1);
869
+ }
870
+ }
871
+ }
872
+ // Re-filter edges after per-file and non-production caps
873
+ finalEdges = finalEdges.filter((e) => finalNodes.has(e.source) && finalNodes.has(e.target));
874
+ // Edge recovery: BFS with many entry points leaves most nodes disconnected.
875
+ // Discover edges between already-selected nodes to recover connectivity.
876
+ const recoveryKinds = ['calls', 'extends', 'implements', 'references', 'overrides'];
877
+ const recoveredEdges = this.queries.findEdgesBetweenNodes([...finalNodes.keys()], recoveryKinds);
878
+ const existingEdgeKeys = new Set(finalEdges.map((e) => `${e.source}:${e.target}:${e.kind}`));
879
+ for (const edge of recoveredEdges) {
880
+ const key = `${edge.source}:${edge.target}:${edge.kind}`;
881
+ if (!existingEdgeKeys.has(key)) {
882
+ finalEdges.push(edge);
883
+ existingEdgeKeys.add(key);
884
+ }
885
+ }
886
+ return { nodes: finalNodes, edges: finalEdges, roots };
887
+ }
888
+ /**
889
+ * Get the source code for a node
890
+ *
891
+ * Reads the file and extracts the code between startLine and endLine.
892
+ *
893
+ * @param nodeId - ID of the node
894
+ * @returns Code string or null if not found
895
+ */
896
+ async getCode(nodeId) {
897
+ const node = this.queries.getNodeById(nodeId);
898
+ if (!node) {
899
+ return null;
900
+ }
901
+ return this.extractNodeCode(node);
902
+ }
903
+ /**
904
+ * Extract code from a node's source file
905
+ */
906
+ async extractNodeCode(node) {
907
+ const filePath = (0, utils_1.validatePathWithinRoot)(this.projectRoot, node.filePath);
908
+ if (!filePath || !fs.existsSync(filePath)) {
909
+ return null;
910
+ }
911
+ try {
912
+ const content = fs.readFileSync(filePath, 'utf-8');
913
+ const lines = content.split('\n');
914
+ // Extract lines (1-indexed to 0-indexed)
915
+ const startIdx = Math.max(0, node.startLine - 1);
916
+ const endIdx = Math.min(lines.length, node.endLine);
917
+ return lines.slice(startIdx, endIdx).join('\n');
918
+ }
919
+ catch (error) {
920
+ (0, errors_1.logDebug)('Failed to extract code from node', { nodeId: node.id, filePath: node.filePath, error: String(error) });
921
+ return null;
922
+ }
923
+ }
924
+ /**
925
+ * Get entry points from a subgraph (the root nodes)
926
+ */
927
+ getEntryPoints(subgraph) {
928
+ return subgraph.roots
929
+ .map((id) => subgraph.nodes.get(id))
930
+ .filter((n) => n !== undefined);
931
+ }
932
+ /**
933
+ * Extract code blocks for key nodes in the subgraph
934
+ */
935
+ async extractCodeBlocks(subgraph, maxBlocks, maxBlockSize) {
936
+ const blocks = [];
937
+ // Prioritize entry points, then functions/methods
938
+ const priorityNodes = [];
939
+ // First: entry points
940
+ for (const id of subgraph.roots) {
941
+ const node = subgraph.nodes.get(id);
942
+ if (node) {
943
+ priorityNodes.push(node);
944
+ }
945
+ }
946
+ // Then: functions and methods
947
+ for (const node of subgraph.nodes.values()) {
948
+ if (!subgraph.roots.includes(node.id)) {
949
+ if (node.kind === 'function' || node.kind === 'method') {
950
+ priorityNodes.push(node);
951
+ }
952
+ }
953
+ }
954
+ // Then: classes
955
+ for (const node of subgraph.nodes.values()) {
956
+ if (!subgraph.roots.includes(node.id)) {
957
+ if (node.kind === 'class') {
958
+ priorityNodes.push(node);
959
+ }
960
+ }
961
+ }
962
+ // Extract code for priority nodes
963
+ for (const node of priorityNodes) {
964
+ if (blocks.length >= maxBlocks)
965
+ break;
966
+ const code = await this.extractNodeCode(node);
967
+ if (code) {
968
+ // Truncate if too long
969
+ const truncated = code.length > maxBlockSize
970
+ ? code.slice(0, maxBlockSize) + '\n// ... truncated ...'
971
+ : code;
972
+ blocks.push({
973
+ content: truncated,
974
+ filePath: node.filePath,
975
+ startLine: node.startLine,
976
+ endLine: node.endLine,
977
+ language: node.language,
978
+ node,
979
+ });
980
+ }
981
+ }
982
+ return blocks;
983
+ }
984
+ /**
985
+ * Get unique files from a subgraph
986
+ */
987
+ getRelatedFiles(subgraph) {
988
+ const files = new Set();
989
+ for (const node of subgraph.nodes.values()) {
990
+ files.add(node.filePath);
991
+ }
992
+ return Array.from(files).sort();
993
+ }
994
+ /**
995
+ * Generate a summary of the context
996
+ */
997
+ generateSummary(_query, subgraph, entryPoints) {
998
+ const nodeCount = subgraph.nodes.size;
999
+ const edgeCount = subgraph.edges.length;
1000
+ const files = this.getRelatedFiles(subgraph);
1001
+ const entryPointNames = entryPoints
1002
+ .slice(0, 3)
1003
+ .map((n) => n.name)
1004
+ .join(', ');
1005
+ const remaining = entryPoints.length > 3 ? ` and ${entryPoints.length - 3} more` : '';
1006
+ return `Found ${nodeCount} relevant code symbols across ${files.length} files. ` +
1007
+ `Key entry points: ${entryPointNames}${remaining}. ` +
1008
+ `${edgeCount} relationships identified.`;
1009
+ }
1010
+ /**
1011
+ * Resolve import/export nodes to their actual definitions
1012
+ *
1013
+ * When search returns `import { TerminalPanel }`, users want the TerminalPanel
1014
+ * class definition, not the import statement. This follows the `imports` edge
1015
+ * to find and return the actual definition instead.
1016
+ *
1017
+ * @param results - Search results that may include import/export nodes
1018
+ * @returns Results with imports resolved to definitions where possible
1019
+ */
1020
+ resolveImportsToDefinitions(results) {
1021
+ const resolved = [];
1022
+ const seenIds = new Set();
1023
+ for (const result of results) {
1024
+ const { node, score } = result;
1025
+ // If it's not an import/export, keep it as-is
1026
+ if (node.kind !== 'import' && node.kind !== 'export') {
1027
+ if (!seenIds.has(node.id)) {
1028
+ seenIds.add(node.id);
1029
+ resolved.push(result);
1030
+ }
1031
+ continue;
1032
+ }
1033
+ // For imports/exports, try to find what they reference
1034
+ // Imports have outgoing 'imports' edges to the definition
1035
+ // Exports have outgoing 'exports' edges to the definition
1036
+ const edgeKind = node.kind === 'import' ? 'imports' : 'exports';
1037
+ const outgoingEdges = this.queries.getOutgoingEdges(node.id, [edgeKind]);
1038
+ let foundDefinition = false;
1039
+ for (const edge of outgoingEdges) {
1040
+ const targetNode = this.queries.getNodeById(edge.target);
1041
+ if (targetNode && !seenIds.has(targetNode.id)) {
1042
+ // Found the definition - use it instead of the import
1043
+ seenIds.add(targetNode.id);
1044
+ resolved.push({
1045
+ node: targetNode,
1046
+ score: score, // Preserve the original score
1047
+ });
1048
+ foundDefinition = true;
1049
+ (0, errors_1.logDebug)('Resolved import to definition', {
1050
+ import: node.name,
1051
+ definition: targetNode.name,
1052
+ kind: targetNode.kind,
1053
+ });
1054
+ }
1055
+ }
1056
+ // If we couldn't resolve the import, skip it (it's low-value on its own)
1057
+ if (!foundDefinition) {
1058
+ (0, errors_1.logDebug)('Skipping unresolved import', { name: node.name, file: node.filePath });
1059
+ }
1060
+ }
1061
+ return resolved;
1062
+ }
1063
+ }
1064
+ exports.ContextBuilder = ContextBuilder;
1065
+ /**
1066
+ * Create a context builder
1067
+ */
1068
+ function createContextBuilder(projectRoot, queries, traverser, vectorManager) {
1069
+ return new ContextBuilder(projectRoot, queries, traverser, vectorManager);
1070
+ }
1071
+ // Re-export formatter
1072
+ var formatter_2 = require("./formatter");
1073
+ Object.defineProperty(exports, "formatContextAsMarkdown", { enumerable: true, get: function () { return formatter_2.formatContextAsMarkdown; } });
1074
+ Object.defineProperty(exports, "formatContextAsJson", { enumerable: true, get: function () { return formatter_2.formatContextAsJson; } });
1075
+ //# sourceMappingURL=index.js.map