@cstart/coldstart 2.0.0

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 (360) hide show
  1. package/README.md +236 -0
  2. package/dist/cache/disk-cache.d.ts +14 -0
  3. package/dist/cache/disk-cache.d.ts.map +1 -0
  4. package/dist/cache/disk-cache.js +371 -0
  5. package/dist/cache/disk-cache.js.map +1 -0
  6. package/dist/cli.d.ts +18 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +206 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/constants.d.ts +8 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +111 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/daemon-lock.d.ts +63 -0
  15. package/dist/daemon-lock.d.ts.map +1 -0
  16. package/dist/daemon-lock.js +218 -0
  17. package/dist/daemon-lock.js.map +1 -0
  18. package/dist/daemon-log.d.ts +26 -0
  19. package/dist/daemon-log.d.ts.map +1 -0
  20. package/dist/daemon-log.js +110 -0
  21. package/dist/daemon-log.js.map +1 -0
  22. package/dist/index-manager.d.ts +103 -0
  23. package/dist/index-manager.d.ts.map +1 -0
  24. package/dist/index-manager.js +361 -0
  25. package/dist/index-manager.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +724 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/indexer/content-tokens.d.ts +74 -0
  31. package/dist/indexer/content-tokens.d.ts.map +1 -0
  32. package/dist/indexer/content-tokens.js +406 -0
  33. package/dist/indexer/content-tokens.js.map +1 -0
  34. package/dist/indexer/cpp-include-roots.d.ts +7 -0
  35. package/dist/indexer/cpp-include-roots.d.ts.map +1 -0
  36. package/dist/indexer/cpp-include-roots.js +166 -0
  37. package/dist/indexer/cpp-include-roots.js.map +1 -0
  38. package/dist/indexer/csharp-synthetic.d.ts +9 -0
  39. package/dist/indexer/csharp-synthetic.d.ts.map +1 -0
  40. package/dist/indexer/csharp-synthetic.js +51 -0
  41. package/dist/indexer/csharp-synthetic.js.map +1 -0
  42. package/dist/indexer/django-synthetic.d.ts +12 -0
  43. package/dist/indexer/django-synthetic.d.ts.map +1 -0
  44. package/dist/indexer/django-synthetic.js +49 -0
  45. package/dist/indexer/django-synthetic.js.map +1 -0
  46. package/dist/indexer/extractors/angularjs.d.ts +16 -0
  47. package/dist/indexer/extractors/angularjs.d.ts.map +1 -0
  48. package/dist/indexer/extractors/angularjs.js +49 -0
  49. package/dist/indexer/extractors/angularjs.js.map +1 -0
  50. package/dist/indexer/extractors/cpp.d.ts +9 -0
  51. package/dist/indexer/extractors/cpp.d.ts.map +1 -0
  52. package/dist/indexer/extractors/cpp.js +254 -0
  53. package/dist/indexer/extractors/cpp.js.map +1 -0
  54. package/dist/indexer/extractors/csharp.d.ts +19 -0
  55. package/dist/indexer/extractors/csharp.d.ts.map +1 -0
  56. package/dist/indexer/extractors/csharp.js +327 -0
  57. package/dist/indexer/extractors/csharp.js.map +1 -0
  58. package/dist/indexer/extractors/env.d.ts +9 -0
  59. package/dist/indexer/extractors/env.d.ts.map +1 -0
  60. package/dist/indexer/extractors/env.js +43 -0
  61. package/dist/indexer/extractors/env.js.map +1 -0
  62. package/dist/indexer/extractors/go.d.ts +9 -0
  63. package/dist/indexer/extractors/go.d.ts.map +1 -0
  64. package/dist/indexer/extractors/go.js +227 -0
  65. package/dist/indexer/extractors/go.js.map +1 -0
  66. package/dist/indexer/extractors/graphql.d.ts +22 -0
  67. package/dist/indexer/extractors/graphql.d.ts.map +1 -0
  68. package/dist/indexer/extractors/graphql.js +88 -0
  69. package/dist/indexer/extractors/graphql.js.map +1 -0
  70. package/dist/indexer/extractors/groovy.d.ts +9 -0
  71. package/dist/indexer/extractors/groovy.d.ts.map +1 -0
  72. package/dist/indexer/extractors/groovy.js +356 -0
  73. package/dist/indexer/extractors/groovy.js.map +1 -0
  74. package/dist/indexer/extractors/java.d.ts +10 -0
  75. package/dist/indexer/extractors/java.d.ts.map +1 -0
  76. package/dist/indexer/extractors/java.js +586 -0
  77. package/dist/indexer/extractors/java.js.map +1 -0
  78. package/dist/indexer/extractors/kotlin.d.ts +10 -0
  79. package/dist/indexer/extractors/kotlin.d.ts.map +1 -0
  80. package/dist/indexer/extractors/kotlin.js +398 -0
  81. package/dist/indexer/extractors/kotlin.js.map +1 -0
  82. package/dist/indexer/extractors/node-helpers.d.ts +13 -0
  83. package/dist/indexer/extractors/node-helpers.d.ts.map +1 -0
  84. package/dist/indexer/extractors/node-helpers.js +17 -0
  85. package/dist/indexer/extractors/node-helpers.js.map +1 -0
  86. package/dist/indexer/extractors/parser-factory.d.ts +4 -0
  87. package/dist/indexer/extractors/parser-factory.d.ts.map +1 -0
  88. package/dist/indexer/extractors/parser-factory.js +28 -0
  89. package/dist/indexer/extractors/parser-factory.js.map +1 -0
  90. package/dist/indexer/extractors/php.d.ts +17 -0
  91. package/dist/indexer/extractors/php.d.ts.map +1 -0
  92. package/dist/indexer/extractors/php.js +363 -0
  93. package/dist/indexer/extractors/php.js.map +1 -0
  94. package/dist/indexer/extractors/python.d.ts +14 -0
  95. package/dist/indexer/extractors/python.d.ts.map +1 -0
  96. package/dist/indexer/extractors/python.js +449 -0
  97. package/dist/indexer/extractors/python.js.map +1 -0
  98. package/dist/indexer/extractors/ruby.d.ts +10 -0
  99. package/dist/indexer/extractors/ruby.d.ts.map +1 -0
  100. package/dist/indexer/extractors/ruby.js +996 -0
  101. package/dist/indexer/extractors/ruby.js.map +1 -0
  102. package/dist/indexer/extractors/rust.d.ts +9 -0
  103. package/dist/indexer/extractors/rust.d.ts.map +1 -0
  104. package/dist/indexer/extractors/rust.js +322 -0
  105. package/dist/indexer/extractors/rust.js.map +1 -0
  106. package/dist/indexer/extractors/toml.d.ts +9 -0
  107. package/dist/indexer/extractors/toml.d.ts.map +1 -0
  108. package/dist/indexer/extractors/toml.js +84 -0
  109. package/dist/indexer/extractors/toml.js.map +1 -0
  110. package/dist/indexer/extractors/xml.d.ts +9 -0
  111. package/dist/indexer/extractors/xml.d.ts.map +1 -0
  112. package/dist/indexer/extractors/xml.js +163 -0
  113. package/dist/indexer/extractors/xml.js.map +1 -0
  114. package/dist/indexer/extractors/yaml.d.ts +9 -0
  115. package/dist/indexer/extractors/yaml.d.ts.map +1 -0
  116. package/dist/indexer/extractors/yaml.js +117 -0
  117. package/dist/indexer/extractors/yaml.js.map +1 -0
  118. package/dist/indexer/git.d.ts +19 -0
  119. package/dist/indexer/git.d.ts.map +1 -0
  120. package/dist/indexer/git.js +55 -0
  121. package/dist/indexer/git.js.map +1 -0
  122. package/dist/indexer/graph.d.ts +7 -0
  123. package/dist/indexer/graph.d.ts.map +1 -0
  124. package/dist/indexer/graph.js +20 -0
  125. package/dist/indexer/graph.js.map +1 -0
  126. package/dist/indexer/indexed-file.d.ts +15 -0
  127. package/dist/indexer/indexed-file.d.ts.map +1 -0
  128. package/dist/indexer/indexed-file.js +50 -0
  129. package/dist/indexer/indexed-file.js.map +1 -0
  130. package/dist/indexer/invariants.d.ts +15 -0
  131. package/dist/indexer/invariants.d.ts.map +1 -0
  132. package/dist/indexer/invariants.js +96 -0
  133. package/dist/indexer/invariants.js.map +1 -0
  134. package/dist/indexer/laravel-synthetic.d.ts +10 -0
  135. package/dist/indexer/laravel-synthetic.d.ts.map +1 -0
  136. package/dist/indexer/laravel-synthetic.js +78 -0
  137. package/dist/indexer/laravel-synthetic.js.map +1 -0
  138. package/dist/indexer/parser.d.ts +4 -0
  139. package/dist/indexer/parser.d.ts.map +1 -0
  140. package/dist/indexer/parser.js +499 -0
  141. package/dist/indexer/parser.js.map +1 -0
  142. package/dist/indexer/patch.d.ts +3 -0
  143. package/dist/indexer/patch.d.ts.map +1 -0
  144. package/dist/indexer/patch.js +291 -0
  145. package/dist/indexer/patch.js.map +1 -0
  146. package/dist/indexer/rails-synthetic.d.ts +11 -0
  147. package/dist/indexer/rails-synthetic.d.ts.map +1 -0
  148. package/dist/indexer/rails-synthetic.js +80 -0
  149. package/dist/indexer/rails-synthetic.js.map +1 -0
  150. package/dist/indexer/reconcile.d.ts +9 -0
  151. package/dist/indexer/reconcile.d.ts.map +1 -0
  152. package/dist/indexer/reconcile.js +96 -0
  153. package/dist/indexer/reconcile.js.map +1 -0
  154. package/dist/indexer/resolvers/cpp.d.ts +15 -0
  155. package/dist/indexer/resolvers/cpp.d.ts.map +1 -0
  156. package/dist/indexer/resolvers/cpp.js +35 -0
  157. package/dist/indexer/resolvers/cpp.js.map +1 -0
  158. package/dist/indexer/resolvers/csharp.d.ts +2 -0
  159. package/dist/indexer/resolvers/csharp.d.ts.map +1 -0
  160. package/dist/indexer/resolvers/csharp.js +89 -0
  161. package/dist/indexer/resolvers/csharp.js.map +1 -0
  162. package/dist/indexer/resolvers/generic.d.ts +10 -0
  163. package/dist/indexer/resolvers/generic.d.ts.map +1 -0
  164. package/dist/indexer/resolvers/generic.js +35 -0
  165. package/dist/indexer/resolvers/generic.js.map +1 -0
  166. package/dist/indexer/resolvers/go.d.ts +2 -0
  167. package/dist/indexer/resolvers/go.d.ts.map +1 -0
  168. package/dist/indexer/resolvers/go.js +171 -0
  169. package/dist/indexer/resolvers/go.js.map +1 -0
  170. package/dist/indexer/resolvers/index.d.ts +26 -0
  171. package/dist/indexer/resolvers/index.d.ts.map +1 -0
  172. package/dist/indexer/resolvers/index.js +301 -0
  173. package/dist/indexer/resolvers/index.js.map +1 -0
  174. package/dist/indexer/resolvers/java.d.ts +17 -0
  175. package/dist/indexer/resolvers/java.d.ts.map +1 -0
  176. package/dist/indexer/resolvers/java.js +104 -0
  177. package/dist/indexer/resolvers/java.js.map +1 -0
  178. package/dist/indexer/resolvers/php.d.ts +2 -0
  179. package/dist/indexer/resolvers/php.d.ts.map +1 -0
  180. package/dist/indexer/resolvers/php.js +108 -0
  181. package/dist/indexer/resolvers/php.js.map +1 -0
  182. package/dist/indexer/resolvers/python.d.ts +17 -0
  183. package/dist/indexer/resolvers/python.d.ts.map +1 -0
  184. package/dist/indexer/resolvers/python.js +63 -0
  185. package/dist/indexer/resolvers/python.js.map +1 -0
  186. package/dist/indexer/resolvers/ruby.d.ts +13 -0
  187. package/dist/indexer/resolvers/ruby.d.ts.map +1 -0
  188. package/dist/indexer/resolvers/ruby.js +199 -0
  189. package/dist/indexer/resolvers/ruby.js.map +1 -0
  190. package/dist/indexer/resolvers/rust.d.ts +12 -0
  191. package/dist/indexer/resolvers/rust.d.ts.map +1 -0
  192. package/dist/indexer/resolvers/rust.js +61 -0
  193. package/dist/indexer/resolvers/rust.js.map +1 -0
  194. package/dist/indexer/resolvers/shared.d.ts +18 -0
  195. package/dist/indexer/resolvers/shared.d.ts.map +1 -0
  196. package/dist/indexer/resolvers/shared.js +71 -0
  197. package/dist/indexer/resolvers/shared.js.map +1 -0
  198. package/dist/indexer/rust-workspace.d.ts +25 -0
  199. package/dist/indexer/rust-workspace.d.ts.map +1 -0
  200. package/dist/indexer/rust-workspace.js +155 -0
  201. package/dist/indexer/rust-workspace.js.map +1 -0
  202. package/dist/indexer/symbol-edges.d.ts +19 -0
  203. package/dist/indexer/symbol-edges.d.ts.map +1 -0
  204. package/dist/indexer/symbol-edges.js +117 -0
  205. package/dist/indexer/symbol-edges.js.map +1 -0
  206. package/dist/indexer/tokenize.d.ts +26 -0
  207. package/dist/indexer/tokenize.d.ts.map +1 -0
  208. package/dist/indexer/tokenize.js +200 -0
  209. package/dist/indexer/tokenize.js.map +1 -0
  210. package/dist/indexer/ts-parser.d.ts +10 -0
  211. package/dist/indexer/ts-parser.d.ts.map +1 -0
  212. package/dist/indexer/ts-parser.js +605 -0
  213. package/dist/indexer/ts-parser.js.map +1 -0
  214. package/dist/indexer/walker.d.ts +9 -0
  215. package/dist/indexer/walker.d.ts.map +1 -0
  216. package/dist/indexer/walker.js +91 -0
  217. package/dist/indexer/walker.js.map +1 -0
  218. package/dist/init.d.ts +111 -0
  219. package/dist/init.d.ts.map +1 -0
  220. package/dist/init.js +787 -0
  221. package/dist/init.js.map +1 -0
  222. package/dist/kb/cli.d.ts +2 -0
  223. package/dist/kb/cli.d.ts.map +1 -0
  224. package/dist/kb/cli.js +377 -0
  225. package/dist/kb/cli.js.map +1 -0
  226. package/dist/kb/commit.d.ts +6 -0
  227. package/dist/kb/commit.d.ts.map +1 -0
  228. package/dist/kb/commit.js +84 -0
  229. package/dist/kb/commit.js.map +1 -0
  230. package/dist/kb/fold.d.ts +42 -0
  231. package/dist/kb/fold.d.ts.map +1 -0
  232. package/dist/kb/fold.js +285 -0
  233. package/dist/kb/fold.js.map +1 -0
  234. package/dist/kb/freshness.d.ts +9 -0
  235. package/dist/kb/freshness.d.ts.map +1 -0
  236. package/dist/kb/freshness.js +52 -0
  237. package/dist/kb/freshness.js.map +1 -0
  238. package/dist/kb/git.d.ts +4 -0
  239. package/dist/kb/git.d.ts.map +1 -0
  240. package/dist/kb/git.js +28 -0
  241. package/dist/kb/git.js.map +1 -0
  242. package/dist/kb/ids.d.ts +8 -0
  243. package/dist/kb/ids.d.ts.map +1 -0
  244. package/dist/kb/ids.js +42 -0
  245. package/dist/kb/ids.js.map +1 -0
  246. package/dist/kb/lint.d.ts +23 -0
  247. package/dist/kb/lint.d.ts.map +1 -0
  248. package/dist/kb/lint.js +102 -0
  249. package/dist/kb/lint.js.map +1 -0
  250. package/dist/kb/lookup.d.ts +55 -0
  251. package/dist/kb/lookup.d.ts.map +1 -0
  252. package/dist/kb/lookup.js +92 -0
  253. package/dist/kb/lookup.js.map +1 -0
  254. package/dist/kb/notes-index.d.ts +31 -0
  255. package/dist/kb/notes-index.d.ts.map +1 -0
  256. package/dist/kb/notes-index.js +0 -0
  257. package/dist/kb/notes-index.js.map +1 -0
  258. package/dist/kb/raw-log.d.ts +34 -0
  259. package/dist/kb/raw-log.d.ts.map +1 -0
  260. package/dist/kb/raw-log.js +106 -0
  261. package/dist/kb/raw-log.js.map +1 -0
  262. package/dist/kb/render.d.ts +17 -0
  263. package/dist/kb/render.d.ts.map +1 -0
  264. package/dist/kb/render.js +101 -0
  265. package/dist/kb/render.js.map +1 -0
  266. package/dist/kb/search.d.ts +110 -0
  267. package/dist/kb/search.d.ts.map +1 -0
  268. package/dist/kb/search.js +453 -0
  269. package/dist/kb/search.js.map +1 -0
  270. package/dist/kb/store.d.ts +35 -0
  271. package/dist/kb/store.d.ts.map +1 -0
  272. package/dist/kb/store.js +160 -0
  273. package/dist/kb/store.js.map +1 -0
  274. package/dist/kb/types.d.ts +142 -0
  275. package/dist/kb/types.d.ts.map +1 -0
  276. package/dist/kb/types.js +13 -0
  277. package/dist/kb/types.js.map +1 -0
  278. package/dist/kb/view-template.d.ts +8 -0
  279. package/dist/kb/view-template.d.ts.map +1 -0
  280. package/dist/kb/view-template.js +8 -0
  281. package/dist/kb/view-template.js.map +1 -0
  282. package/dist/kb/view.d.ts +42 -0
  283. package/dist/kb/view.d.ts.map +1 -0
  284. package/dist/kb/view.js +118 -0
  285. package/dist/kb/view.js.map +1 -0
  286. package/dist/kb/write.d.ts +72 -0
  287. package/dist/kb/write.d.ts.map +1 -0
  288. package/dist/kb/write.js +345 -0
  289. package/dist/kb/write.js.map +1 -0
  290. package/dist/keeper-state.d.ts +29 -0
  291. package/dist/keeper-state.d.ts.map +1 -0
  292. package/dist/keeper-state.js +89 -0
  293. package/dist/keeper-state.js.map +1 -0
  294. package/dist/keeper.d.ts +23 -0
  295. package/dist/keeper.d.ts.map +1 -0
  296. package/dist/keeper.js +105 -0
  297. package/dist/keeper.js.map +1 -0
  298. package/dist/migrate.d.ts +9 -0
  299. package/dist/migrate.d.ts.map +1 -0
  300. package/dist/migrate.js +102 -0
  301. package/dist/migrate.js.map +1 -0
  302. package/dist/restart.d.ts +15 -0
  303. package/dist/restart.d.ts.map +1 -0
  304. package/dist/restart.js +48 -0
  305. package/dist/restart.js.map +1 -0
  306. package/dist/server/find.d.ts +39 -0
  307. package/dist/server/find.d.ts.map +1 -0
  308. package/dist/server/find.js +979 -0
  309. package/dist/server/find.js.map +1 -0
  310. package/dist/server/mcp.d.ts +117 -0
  311. package/dist/server/mcp.d.ts.map +1 -0
  312. package/dist/server/mcp.js +301 -0
  313. package/dist/server/mcp.js.map +1 -0
  314. package/dist/server/searcher.d.ts +14 -0
  315. package/dist/server/searcher.d.ts.map +1 -0
  316. package/dist/server/searcher.js +146 -0
  317. package/dist/server/searcher.js.map +1 -0
  318. package/dist/server/tools.d.ts +17 -0
  319. package/dist/server/tools.d.ts.map +1 -0
  320. package/dist/server/tools.js +875 -0
  321. package/dist/server/tools.js.map +1 -0
  322. package/dist/status.d.ts +12 -0
  323. package/dist/status.d.ts.map +1 -0
  324. package/dist/status.js +157 -0
  325. package/dist/status.js.map +1 -0
  326. package/dist/types.d.ts +138 -0
  327. package/dist/types.d.ts.map +1 -0
  328. package/dist/types.js +3 -0
  329. package/dist/types.js.map +1 -0
  330. package/dist/watcher.d.ts +11 -0
  331. package/dist/watcher.d.ts.map +1 -0
  332. package/dist/watcher.js +67 -0
  333. package/dist/watcher.js.map +1 -0
  334. package/hooks/canonical-find-key.mjs +68 -0
  335. package/hooks/codex-find-nudge.mjs +15 -0
  336. package/hooks/codex-find-preguard.mjs +14 -0
  337. package/hooks/codex-kb-elicit.mjs +329 -0
  338. package/hooks/codex-kb-recall.mjs +151 -0
  339. package/hooks/codex-nudge-handler.mjs +370 -0
  340. package/hooks/codex-preguard-handler.mjs +75 -0
  341. package/hooks/codex-run-hook.mjs +132 -0
  342. package/hooks/coldstart-call.mjs +58 -0
  343. package/hooks/cursor-find-nudge.mjs +15 -0
  344. package/hooks/cursor-find-preguard.mjs +14 -0
  345. package/hooks/cursor-input.mjs +72 -0
  346. package/hooks/cursor-kb-elicit.mjs +335 -0
  347. package/hooks/cursor-kb-recall.mjs +138 -0
  348. package/hooks/cursor-nudge-handler.mjs +33 -0
  349. package/hooks/cursor-preguard-handler.mjs +39 -0
  350. package/hooks/cursor-run-hook.mjs +129 -0
  351. package/hooks/find-nudge.mjs +15 -0
  352. package/hooks/find-preguard.mjs +14 -0
  353. package/hooks/kb-elicit.mjs +334 -0
  354. package/hooks/kb-recall.mjs +151 -0
  355. package/hooks/nudge-handler.mjs +370 -0
  356. package/hooks/preguard-handler.mjs +75 -0
  357. package/hooks/run-hook.mjs +132 -0
  358. package/package.json +69 -0
  359. package/templates/coldstart.cli.md +60 -0
  360. package/templates/coldstart.mcp.md +59 -0
@@ -0,0 +1,996 @@
1
+ /**
2
+ * Tree-sitter based parser for Ruby.
3
+ * Extracts symbol-level nodes (classes, modules, methods, constants)
4
+ * and their relationships (calls, extends, includes/implements).
5
+ *
6
+ * Follows the same interface and patterns as ts-parser.ts.
7
+ */
8
+ import { createRequire } from 'node:module';
9
+ import { childrenOfType, firstChildOfType, firstChildOfTypes } from './node-helpers.js';
10
+ import { makeParser } from './parser-factory.js';
11
+ const require = createRequire(import.meta.url);
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ const rubyGrammar = require('tree-sitter-ruby');
14
+ const getParser = makeParser(rubyGrammar);
15
+ // ---------------------------------------------------------------------------
16
+ // Node helpers
17
+ // ---------------------------------------------------------------------------
18
+ /** Collect all call node method names + first-seen line in a subtree */
19
+ function collectCalls(node, results) {
20
+ if (node.type === 'call') {
21
+ // call grammar: field('receiver', ...) '.' field('method', identifier|constant) field('arguments', ...)?
22
+ // Use the field accessor — `find(...)` returns the receiver when it's a bare
23
+ // identifier/constant, not the method name.
24
+ const methodNode = node.childForFieldName('method');
25
+ if (methodNode && !results.has(methodNode.text)) {
26
+ results.set(methodNode.text, node.startPosition.row + 1);
27
+ }
28
+ }
29
+ else if (node.type === 'method_call' || node.type === 'command') {
30
+ // bare method call: method_name args
31
+ const methodNode = node.namedChildren[0];
32
+ if (methodNode?.type === 'identifier' && !results.has(methodNode.text)) {
33
+ results.set(methodNode.text, node.startPosition.row + 1);
34
+ }
35
+ }
36
+ for (const child of node.namedChildren) {
37
+ collectCalls(child, results);
38
+ }
39
+ }
40
+ function callsFromMap(calls, exclude) {
41
+ const out = [];
42
+ for (const [name, line] of calls) {
43
+ if (exclude && name === exclude)
44
+ continue;
45
+ out.push({ name, line });
46
+ }
47
+ return out;
48
+ }
49
+ /** Get constant name from a scope_resolution or constant node */
50
+ function getConstantName(node) {
51
+ if (!node)
52
+ return null;
53
+ if (node.type === 'constant')
54
+ return node.text;
55
+ if (node.type === 'scope_resolution') {
56
+ // Foo::Bar — return the full path
57
+ return node.text;
58
+ }
59
+ return null;
60
+ }
61
+ // ---------------------------------------------------------------------------
62
+ // Rails DSL detection helpers
63
+ // ---------------------------------------------------------------------------
64
+ const RAILS_ASSOCIATION_METHODS = new Set(['has_many', 'has_one', 'belongs_to', 'has_and_belongs_to_many']);
65
+ const RAILS_CALLBACK_METHODS = new Set(['before_action', 'after_action', 'around_action', 'before_filter', 'after_filter', 'validates', 'validate']);
66
+ const RAILS_DSL_METHODS = new Set(['attr_accessor', 'attr_reader', 'attr_writer', 'delegate', 'scope', 'attribute', 'enum']);
67
+ // Stoplist for Rails autoload constant resolution — framework/stdlib constants we never resolve
68
+ const CONSTANT_STOPLIST_TOP_LEVEL = new Set([
69
+ // Ruby builtins
70
+ 'Object', 'Kernel', 'Module', 'Class', 'BasicObject', 'Proc', 'Method', 'UnboundMethod',
71
+ 'String', 'Symbol', 'Integer', 'Float', 'Numeric', 'Rational', 'Complex',
72
+ 'TrueClass', 'FalseClass', 'NilClass', 'Array', 'Hash', 'Set', 'Range',
73
+ 'Regexp', 'MatchData', 'Time', 'Date', 'DateTime', 'IO', 'File', 'Dir',
74
+ 'Pathname', 'URI', 'Tempfile', 'Struct', 'OpenStruct', 'Comparable',
75
+ 'Enumerable', 'Enumerator', 'StandardError', 'RuntimeError', 'ArgumentError',
76
+ 'TypeError', 'NameError', 'NoMethodError', 'NotImplementedError', 'IOError',
77
+ 'EOFError', 'SystemCallError', 'ZeroDivisionError', 'FloatDomainError',
78
+ 'IndexError', 'KeyError', 'RangeError', 'StopIteration', 'ThreadError',
79
+ 'FiberError', 'LocalJumpError', 'SignalException', 'Errno', 'Encoding',
80
+ 'Thread', 'Fiber', 'Mutex', 'Queue', 'ConditionVariable', 'GC',
81
+ 'ObjectSpace', 'Process', 'Signal', 'Math', 'Random', 'JSON', 'YAML',
82
+ 'CSV', 'Base64', 'Digest', 'OpenSSL', 'Net', 'ERB', 'CGI', 'Logger',
83
+ 'SecureRandom', 'FileUtils', 'Forwardable', 'Singleton', 'BigDecimal',
84
+ 'Marshal',
85
+ // Rails framework
86
+ 'Rails', 'ActiveRecord', 'ActiveModel', 'ActiveSupport', 'ActionController',
87
+ 'ActionView', 'ActionDispatch', 'ActionMailer', 'ActionCable', 'ActionPack',
88
+ 'ActiveJob', 'ActiveStorage', 'ActionText', 'ActionMailbox',
89
+ 'Concern', 'Mime', 'MIME', 'I18n', 'Migration', 'Schema', 'Devise',
90
+ 'Doorkeeper', 'Sidekiq', 'Paperclip', 'CarrierWave', 'Pundit', 'Kaminari',
91
+ 'WillPaginate', 'RSpec', 'Minitest', 'Faker', 'FactoryBot', 'Webpacker',
92
+ 'RSolr', 'Redis', 'Memcached', 'Resque', 'GoodJob', 'Que', 'DelayedJob',
93
+ 'Aws', 'Azure', 'Google', 'GraphQL', 'Stripe', 'Twilio', 'Twitter',
94
+ 'OmniAuth', 'Bundler', 'Gem', 'Rake', 'Rack', 'Sprockets',
95
+ 'Mail', 'Nokogiri', 'HTTP', 'HTTParty', 'Faraday', 'Chewy', 'Elasticsearch',
96
+ ]);
97
+ const CONSTANT_STOPLIST_PREFIXES = [
98
+ 'ActiveRecord::', 'ActiveModel::', 'ActiveSupport::', 'ActionController::',
99
+ 'ActionView::', 'ActionDispatch::', 'ActionMailer::', 'ActionCable::',
100
+ 'ActiveJob::', 'ActiveStorage::', 'Rails::', 'Concern::',
101
+ 'Devise::', 'Doorkeeper::', 'Sidekiq::', 'Pundit::', 'Kaminari::',
102
+ 'OmniAuth::', 'OAuth::', 'OAuth2::', 'JWT::', 'OpenSSL::', 'Net::',
103
+ 'URI::', 'Errno::', 'Encoding::', 'Process::', 'Math::', 'File::',
104
+ 'IO::', 'Dir::', 'Thread::', 'JSON::', 'YAML::', 'CSV::', 'ERB::',
105
+ 'Digest::', 'Base64::', 'Mime::', 'MIME::', 'I18n::', 'Aws::',
106
+ 'Google::', 'GraphQL::', 'Stripe::', 'Mail::', 'Nokogiri::',
107
+ 'HTTP::', 'HTTParty::', 'Faraday::', 'Chewy::', 'Sprockets::',
108
+ 'Bundler::', 'Gem::', 'Rake::', 'Rack::', 'Logger::', 'FactoryBot::', 'RSpec::', 'Minitest::',
109
+ ];
110
+ function isConstantStoplisted(fqcn) {
111
+ const head = fqcn.split('::')[0];
112
+ if (CONSTANT_STOPLIST_TOP_LEVEL.has(head))
113
+ return true;
114
+ for (const p of CONSTANT_STOPLIST_PREFIXES)
115
+ if (fqcn.startsWith(p))
116
+ return true;
117
+ return false;
118
+ }
119
+ /** Convert snake_case association name to CamelCase model name */
120
+ function associationToModel(name) {
121
+ // has_many :comments → Comment; has_one :profile → Profile
122
+ // Singularize the name if it's plural, convert to CamelCase
123
+ const singular = singularize(name);
124
+ return singular.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
125
+ .replace(/^(.)/, (_, c) => c.toUpperCase());
126
+ }
127
+ /** Singularize plural English nouns (basic rules for Rails conventions) */
128
+ function singularize(word) {
129
+ // Leave already-singular words alone: -us (status, virus, focus), -ss (address, class),
130
+ // -is (basis, axis), -ous (famous). Rails Inflector also treats these as uncountable/no-op.
131
+ if (word.endsWith('us') || word.endsWith('ss') || word.endsWith('is'))
132
+ return word;
133
+ if (word.endsWith('ies'))
134
+ return word.slice(0, -3) + 'y';
135
+ if (word.endsWith('ches') || word.endsWith('shes') || word.endsWith('sses') || word.endsWith('xes') || word.endsWith('zes'))
136
+ return word.slice(0, -2);
137
+ if (word.endsWith('es') && (word.endsWith('oes') || word.endsWith('zes') || word[word.length - 3] === 's' || word[word.length - 3] === 'x' || word[word.length - 3] === 'z' || word[word.length - 3] === 'h'))
138
+ return word.slice(0, -2);
139
+ if (word.endsWith('s'))
140
+ return word.slice(0, -1);
141
+ return word;
142
+ }
143
+ /** Pluralize singular English nouns (basic rules for Rails conventions) */
144
+ function pluralize(word) {
145
+ if (word.endsWith('y'))
146
+ return word.slice(0, -1) + 'ies';
147
+ if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') || word.endsWith('ch') || word.endsWith('sh'))
148
+ return word + 'es';
149
+ return word + 's';
150
+ }
151
+ /** Extract Rails model association file imports from config/routes.rb */
152
+ function extractRoutesImports(rootNode, _fileId) {
153
+ const routeImports = [];
154
+ function walkNode(node) {
155
+ if (node.type === 'call' || node.type === 'command') {
156
+ const methodNode = node.type === 'call'
157
+ ? node.namedChildren.find((c) => c.type === 'identifier')
158
+ : node.namedChildren[0];
159
+ if (!methodNode || methodNode.type !== 'identifier') {
160
+ for (const child of node.namedChildren)
161
+ walkNode(child);
162
+ return;
163
+ }
164
+ const methodName = methodNode.text;
165
+ if (methodName === 'resources' || methodName === 'resource') {
166
+ const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
167
+ const firstArg = args?.namedChildren[0] ?? node.namedChildren[1];
168
+ if (firstArg?.type === 'simple_symbol') {
169
+ const resourceName = firstArg.text.replace(/^:/, '');
170
+ const controllerName = methodName === 'resource' ? pluralize(resourceName) : resourceName;
171
+ routeImports.push(`../app/controllers/${controllerName}_controller`);
172
+ }
173
+ }
174
+ else if (['get', 'post', 'put', 'patch', 'delete'].includes(methodName)) {
175
+ const toTargetRef = { value: null };
176
+ function findToValue(n) {
177
+ if (toTargetRef.value !== null)
178
+ return;
179
+ if (n.type === 'pair' && n.namedChildren.length >= 2) {
180
+ const firstChild = n.namedChildren[0];
181
+ const secondChild = n.namedChildren[1];
182
+ const keyText = firstChild.type === 'hash_key_symbol' ? firstChild.text : (firstChild.text.startsWith(':') ? firstChild.text.slice(1) : firstChild.text);
183
+ if ((firstChild.type === 'hash_key_symbol' || firstChild.type === 'simple_symbol') && keyText === 'to') {
184
+ if (secondChild.type === 'string' || secondChild.type === 'string_literal') {
185
+ const content = firstChildOfType(secondChild, 'string_content');
186
+ toTargetRef.value = content?.text ?? secondChild.text.replace(/^['"]|['"]$/g, '');
187
+ }
188
+ }
189
+ }
190
+ for (const child of n.namedChildren) {
191
+ findToValue(child);
192
+ }
193
+ }
194
+ findToValue(node);
195
+ const toTarget = toTargetRef.value;
196
+ if (toTarget && toTarget.includes('#')) {
197
+ const [controller, _action] = toTarget.split('#');
198
+ const parts = controller.split('/');
199
+ const fileName = parts[parts.length - 1];
200
+ const dir = parts.slice(0, -1).join('/');
201
+ const controllerPath = dir ? `../app/controllers/${dir}/${fileName}_controller` : `../app/controllers/${fileName}_controller`;
202
+ routeImports.push(controllerPath);
203
+ }
204
+ }
205
+ }
206
+ for (const child of node.namedChildren) {
207
+ walkNode(child);
208
+ }
209
+ }
210
+ walkNode(rootNode);
211
+ return routeImports;
212
+ }
213
+ /** Extract DSL method names from command node arguments (bare method call form) */
214
+ function extractDslMethodsFromCommand(methodName, parentName, node, ctx, symbols) {
215
+ if (!parentName)
216
+ return;
217
+ const startLine = node.startPosition.row + 1;
218
+ const endLine = node.endPosition.row + 1;
219
+ const argList = node.namedChildren.slice(1);
220
+ if (methodName === 'attr_accessor') {
221
+ for (const arg of argList) {
222
+ if (arg.type === 'simple_symbol') {
223
+ const propName = arg.text.replace(/^:/, '');
224
+ symbols.push({
225
+ id: `${ctx.fileId}#${parentName}.${propName}`,
226
+ name: `${parentName}.${propName}`,
227
+ kind: 'method',
228
+ startLine,
229
+ endLine,
230
+ isExported: false,
231
+ calls: [],
232
+ implementsNames: [],
233
+ });
234
+ symbols.push({
235
+ id: `${ctx.fileId}#${parentName}.${propName}=`,
236
+ name: `${parentName}.${propName}=`,
237
+ kind: 'method',
238
+ startLine,
239
+ endLine,
240
+ isExported: false,
241
+ calls: [],
242
+ implementsNames: [],
243
+ });
244
+ }
245
+ }
246
+ }
247
+ else if (methodName === 'attr_reader') {
248
+ for (const arg of argList) {
249
+ if (arg.type === 'simple_symbol') {
250
+ const propName = arg.text.replace(/^:/, '');
251
+ symbols.push({
252
+ id: `${ctx.fileId}#${parentName}.${propName}`,
253
+ name: `${parentName}.${propName}`,
254
+ kind: 'method',
255
+ startLine,
256
+ endLine,
257
+ isExported: false,
258
+ calls: [],
259
+ implementsNames: [],
260
+ });
261
+ }
262
+ }
263
+ }
264
+ else if (methodName === 'attr_writer') {
265
+ for (const arg of argList) {
266
+ if (arg.type === 'simple_symbol') {
267
+ const propName = arg.text.replace(/^:/, '');
268
+ symbols.push({
269
+ id: `${ctx.fileId}#${parentName}.${propName}=`,
270
+ name: `${parentName}.${propName}=`,
271
+ kind: 'method',
272
+ startLine,
273
+ endLine,
274
+ isExported: false,
275
+ calls: [],
276
+ implementsNames: [],
277
+ });
278
+ }
279
+ }
280
+ }
281
+ else if (methodName === 'delegate') {
282
+ for (const arg of argList) {
283
+ if (arg.type === 'simple_symbol') {
284
+ const delegatedName = arg.text.replace(/^:/, '');
285
+ symbols.push({
286
+ id: `${ctx.fileId}#${parentName}.${delegatedName}`,
287
+ name: `${parentName}.${delegatedName}`,
288
+ kind: 'method',
289
+ startLine,
290
+ endLine,
291
+ isExported: false,
292
+ calls: [],
293
+ implementsNames: [],
294
+ });
295
+ }
296
+ else {
297
+ break;
298
+ }
299
+ }
300
+ }
301
+ else if (methodName === 'scope') {
302
+ const firstArg = argList[0];
303
+ if (firstArg?.type === 'simple_symbol') {
304
+ const scopeName = firstArg.text.replace(/^:/, '');
305
+ symbols.push({
306
+ id: `${ctx.fileId}#${parentName}.${scopeName}`,
307
+ name: `${parentName}.${scopeName}`,
308
+ kind: 'method',
309
+ startLine,
310
+ endLine,
311
+ isExported: false,
312
+ calls: [],
313
+ implementsNames: [],
314
+ });
315
+ }
316
+ }
317
+ else if (methodName === 'attribute') {
318
+ const firstArg = argList[0];
319
+ if (firstArg?.type === 'simple_symbol') {
320
+ const attrName = firstArg.text.replace(/^:/, '');
321
+ symbols.push({
322
+ id: `${ctx.fileId}#${parentName}.${attrName}`,
323
+ name: `${parentName}.${attrName}`,
324
+ kind: 'method',
325
+ startLine,
326
+ endLine,
327
+ isExported: false,
328
+ calls: [],
329
+ implementsNames: [],
330
+ });
331
+ }
332
+ }
333
+ else if (methodName === 'enum') {
334
+ const firstArg = argList[0];
335
+ if (firstArg?.type === 'pair') {
336
+ const keyNode = firstChildOfTypes(firstArg, ['hash_key_symbol', 'simple_symbol', 'symbol']);
337
+ if (keyNode) {
338
+ const enumName = keyNode.text.replace(/^:/, '');
339
+ symbols.push({
340
+ id: `${ctx.fileId}#${parentName}.${enumName}`,
341
+ name: `${parentName}.${enumName}`,
342
+ kind: 'method',
343
+ startLine,
344
+ endLine,
345
+ isExported: false,
346
+ calls: [],
347
+ implementsNames: [],
348
+ });
349
+ const valueNode = firstChildOfType(firstArg, 'hash');
350
+ if (valueNode) {
351
+ const valuePairs = childrenOfType(valueNode, 'pair');
352
+ for (const pair of valuePairs) {
353
+ const pairKey = firstChildOfTypes(pair, ['hash_key_symbol', 'simple_symbol', 'symbol']);
354
+ if (pairKey) {
355
+ const valueName = pairKey.text.replace(/^:/, '');
356
+ symbols.push({
357
+ id: `${ctx.fileId}#${parentName}.${valueName}?`,
358
+ name: `${parentName}.${valueName}?`,
359
+ kind: 'method',
360
+ startLine,
361
+ endLine,
362
+ isExported: false,
363
+ calls: [],
364
+ implementsNames: [],
365
+ });
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ /** Extract synthesized method names from Rails DSL calls and emit SymbolNode for each */
374
+ function extractDslMethods(node, methodName, parentName, ctx, symbols) {
375
+ if (!parentName)
376
+ return;
377
+ const startLine = node.startPosition.row + 1;
378
+ const endLine = node.endPosition.row + 1;
379
+ const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
380
+ if (!args)
381
+ return;
382
+ const argList = args.namedChildren;
383
+ if (methodName === 'attr_accessor') {
384
+ // attr_accessor :name, :age → emit name, name=, age, age=
385
+ for (const arg of argList) {
386
+ if (arg.type === 'simple_symbol') {
387
+ const propName = arg.text.replace(/^:/, '');
388
+ symbols.push({
389
+ id: `${ctx.fileId}#${parentName}.${propName}`,
390
+ name: `${parentName}.${propName}`,
391
+ kind: 'method',
392
+ startLine,
393
+ endLine,
394
+ isExported: false,
395
+ calls: [],
396
+ implementsNames: [],
397
+ });
398
+ symbols.push({
399
+ id: `${ctx.fileId}#${parentName}.${propName}=`,
400
+ name: `${parentName}.${propName}=`,
401
+ kind: 'method',
402
+ startLine,
403
+ endLine,
404
+ isExported: false,
405
+ calls: [],
406
+ implementsNames: [],
407
+ });
408
+ }
409
+ }
410
+ }
411
+ else if (methodName === 'attr_reader') {
412
+ // attr_reader :name, :age → emit name, age
413
+ for (const arg of argList) {
414
+ if (arg.type === 'simple_symbol') {
415
+ const propName = arg.text.replace(/^:/, '');
416
+ symbols.push({
417
+ id: `${ctx.fileId}#${parentName}.${propName}`,
418
+ name: `${parentName}.${propName}`,
419
+ kind: 'method',
420
+ startLine,
421
+ endLine,
422
+ isExported: false,
423
+ calls: [],
424
+ implementsNames: [],
425
+ });
426
+ }
427
+ }
428
+ }
429
+ else if (methodName === 'attr_writer') {
430
+ // attr_writer :name, :age → emit name=, age=
431
+ for (const arg of argList) {
432
+ if (arg.type === 'simple_symbol') {
433
+ const propName = arg.text.replace(/^:/, '');
434
+ symbols.push({
435
+ id: `${ctx.fileId}#${parentName}.${propName}=`,
436
+ name: `${parentName}.${propName}=`,
437
+ kind: 'method',
438
+ startLine,
439
+ endLine,
440
+ isExported: false,
441
+ calls: [],
442
+ implementsNames: [],
443
+ });
444
+ }
445
+ }
446
+ }
447
+ else if (methodName === 'delegate') {
448
+ // delegate :foo, :bar, to: :user → emit foo, bar (stop at first non-symbol arg)
449
+ for (const arg of argList) {
450
+ if (arg.type === 'simple_symbol') {
451
+ const delegatedName = arg.text.replace(/^:/, '');
452
+ symbols.push({
453
+ id: `${ctx.fileId}#${parentName}.${delegatedName}`,
454
+ name: `${parentName}.${delegatedName}`,
455
+ kind: 'method',
456
+ startLine,
457
+ endLine,
458
+ isExported: false,
459
+ calls: [],
460
+ implementsNames: [],
461
+ });
462
+ }
463
+ else {
464
+ // Stop at first non-symbol arg (hash with to:)
465
+ break;
466
+ }
467
+ }
468
+ }
469
+ else if (methodName === 'scope') {
470
+ // scope :recent, -> { ... } → emit recent (class method)
471
+ const firstArg = argList[0];
472
+ if (firstArg?.type === 'simple_symbol') {
473
+ const scopeName = firstArg.text.replace(/^:/, '');
474
+ symbols.push({
475
+ id: `${ctx.fileId}#${parentName}.${scopeName}`,
476
+ name: `${parentName}.${scopeName}`,
477
+ kind: 'method',
478
+ startLine,
479
+ endLine,
480
+ isExported: false,
481
+ calls: [],
482
+ implementsNames: [],
483
+ });
484
+ }
485
+ }
486
+ else if (methodName === 'attribute') {
487
+ // attribute :webfinger, :string → emit webfinger
488
+ const firstArg = argList[0];
489
+ if (firstArg?.type === 'simple_symbol') {
490
+ const attrName = firstArg.text.replace(/^:/, '');
491
+ symbols.push({
492
+ id: `${ctx.fileId}#${parentName}.${attrName}`,
493
+ name: `${parentName}.${attrName}`,
494
+ kind: 'method',
495
+ startLine,
496
+ endLine,
497
+ isExported: false,
498
+ calls: [],
499
+ implementsNames: [],
500
+ });
501
+ }
502
+ }
503
+ else if (methodName === 'enum') {
504
+ // enum status: { active: 0, inactive: 1 } → emit status, active?, inactive?
505
+ const firstArg = argList[0];
506
+ if (firstArg?.type === 'pair') {
507
+ const keyNode = firstChildOfTypes(firstArg, ['hash_key_symbol', 'simple_symbol', 'symbol']);
508
+ if (keyNode) {
509
+ const enumName = keyNode.text.replace(/^:/, '');
510
+ symbols.push({
511
+ id: `${ctx.fileId}#${parentName}.${enumName}`,
512
+ name: `${parentName}.${enumName}`,
513
+ kind: 'method',
514
+ startLine,
515
+ endLine,
516
+ isExported: false,
517
+ calls: [],
518
+ implementsNames: [],
519
+ });
520
+ const valueNode = firstChildOfType(firstArg, 'hash');
521
+ if (valueNode) {
522
+ const valuePairs = childrenOfType(valueNode, 'pair');
523
+ for (const pair of valuePairs) {
524
+ const pairKey = firstChildOfTypes(pair, ['hash_key_symbol', 'simple_symbol', 'symbol']);
525
+ if (pairKey) {
526
+ const valueName = pairKey.text.replace(/^:/, '');
527
+ symbols.push({
528
+ id: `${ctx.fileId}#${parentName}.${valueName}?`,
529
+ name: `${parentName}.${valueName}?`,
530
+ kind: 'method',
531
+ startLine,
532
+ endLine,
533
+ isExported: false,
534
+ calls: [],
535
+ implementsNames: [],
536
+ });
537
+ }
538
+ }
539
+ }
540
+ }
541
+ }
542
+ }
543
+ }
544
+ // ---------------------------------------------------------------------------
545
+ // Extract method definition
546
+ // ---------------------------------------------------------------------------
547
+ function extractMethod(node, ctx, parentName) {
548
+ const startLine = node.startPosition.row + 1;
549
+ const endLine = node.endPosition.row + 1;
550
+ let methodName;
551
+ let isSingleton = false;
552
+ if (node.type === 'singleton_method') {
553
+ // def self.foo; singleton_method has: object '.' name body_statement
554
+ isSingleton = true;
555
+ const nameNode = node.namedChildren.find((c) => c.type === 'identifier');
556
+ if (!nameNode)
557
+ return null;
558
+ methodName = nameNode.text;
559
+ }
560
+ else {
561
+ // method: 'def' name body_statement 'end'
562
+ const nameNode = firstChildOfTypes(node, ['identifier', 'operator']);
563
+ if (!nameNode)
564
+ return null;
565
+ methodName = nameNode.text;
566
+ }
567
+ const fullName = parentName
568
+ ? isSingleton
569
+ ? `${parentName}.self.${methodName}`
570
+ : `${parentName}.${methodName}`
571
+ : methodName;
572
+ const calls = new Map();
573
+ const body = firstChildOfTypes(node, ['body_statement', 'do_block']);
574
+ if (body)
575
+ collectCalls(body, calls);
576
+ // also scan the whole node for calls
577
+ if (!body)
578
+ collectCalls(node, calls);
579
+ return {
580
+ id: `${ctx.fileId}#${fullName}`,
581
+ name: fullName,
582
+ kind: parentName ? 'method' : 'function',
583
+ startLine,
584
+ endLine,
585
+ isExported: !parentName, // top-level methods are "exported" by default in Ruby
586
+ calls: callsFromMap(calls, methodName),
587
+ implementsNames: [],
588
+ };
589
+ }
590
+ // ---------------------------------------------------------------------------
591
+ // Extract a class or module body
592
+ // ---------------------------------------------------------------------------
593
+ function extractBody(bodyNode, ctx, parentName, symbols) {
594
+ if (!bodyNode)
595
+ return;
596
+ for (const child of bodyNode.namedChildren) {
597
+ extractNode(child, ctx, parentName, symbols);
598
+ }
599
+ }
600
+ // ---------------------------------------------------------------------------
601
+ // Extract a single node
602
+ // ---------------------------------------------------------------------------
603
+ function extractNode(node, ctx, parentName, symbols) {
604
+ const startLine = node.startPosition.row + 1;
605
+ const endLine = node.endPosition.row + 1;
606
+ switch (node.type) {
607
+ case 'class': {
608
+ // class Foo [< Bar] ... end
609
+ const nameNode = firstChildOfTypes(node, ['constant', 'scope_resolution']);
610
+ if (!nameNode)
611
+ return;
612
+ const name = parentName ? `${parentName}::${nameNode.text}` : nameNode.text;
613
+ // superclass: class Foo < Bar — tree-sitter exposes the superclass as a child
614
+ let extendsName;
615
+ const superclassNode = firstChildOfType(node, 'superclass');
616
+ if (superclassNode) {
617
+ const superNode = firstChildOfTypes(superclassNode, ['constant', 'scope_resolution']);
618
+ if (superNode)
619
+ extendsName = superNode.text;
620
+ }
621
+ const implementsNames = [];
622
+ const classSymbol = {
623
+ id: `${ctx.fileId}#${name}`,
624
+ name,
625
+ kind: 'class',
626
+ startLine,
627
+ endLine,
628
+ isExported: true, // Ruby classes are always public
629
+ calls: [],
630
+ extendsName,
631
+ implementsNames,
632
+ };
633
+ symbols.push(classSymbol);
634
+ // Extract body
635
+ const body = firstChildOfType(node, 'body_statement');
636
+ if (body)
637
+ extractBody(body, ctx, name, symbols);
638
+ break;
639
+ }
640
+ case 'module': {
641
+ const nameNode = firstChildOfTypes(node, ['constant', 'scope_resolution']);
642
+ if (!nameNode)
643
+ return;
644
+ const name = parentName ? `${parentName}::${nameNode.text}` : nameNode.text;
645
+ const modSymbol = {
646
+ id: `${ctx.fileId}#${name}`,
647
+ name,
648
+ kind: 'class', // modules modeled as class
649
+ startLine,
650
+ endLine,
651
+ isExported: true,
652
+ calls: [],
653
+ implementsNames: [],
654
+ };
655
+ symbols.push(modSymbol);
656
+ const body = firstChildOfType(node, 'body_statement');
657
+ if (body)
658
+ extractBody(body, ctx, name, symbols);
659
+ break;
660
+ }
661
+ case 'method':
662
+ case 'singleton_method': {
663
+ const sym = extractMethod(node, ctx, parentName);
664
+ if (sym)
665
+ symbols.push(sym);
666
+ break;
667
+ }
668
+ case 'assignment': {
669
+ // Constant assignment: FOO = ...
670
+ const leftNode = node.namedChildren[0];
671
+ if (!leftNode)
672
+ return;
673
+ // Constants start with uppercase
674
+ if (leftNode.type === 'constant' && /^[A-Z]/.test(leftNode.text)) {
675
+ const constName = parentName ? `${parentName}::${leftNode.text}` : leftNode.text;
676
+ symbols.push({
677
+ id: `${ctx.fileId}#${constName}`,
678
+ name: constName,
679
+ kind: 'constant',
680
+ startLine,
681
+ endLine,
682
+ isExported: true,
683
+ calls: [],
684
+ implementsNames: [],
685
+ });
686
+ }
687
+ break;
688
+ }
689
+ case 'call': {
690
+ // Detect Rails DSLs: include/extend/prepend (implement edges)
691
+ // and has_many/belongs_to/before_action etc.
692
+ if (!parentName)
693
+ return;
694
+ const receiver = node.namedChildren[0];
695
+ const methodNode = node.namedChildren.find((c) => c.type === 'identifier');
696
+ if (!methodNode)
697
+ return;
698
+ const methodName = methodNode.text;
699
+ if (methodName === 'include' || methodName === 'extend' || methodName === 'prepend') {
700
+ // include Foo → implements edge
701
+ const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
702
+ if (args) {
703
+ for (const arg of args.namedChildren) {
704
+ const modName = getConstantName(arg);
705
+ if (modName) {
706
+ // Add to parent class's implementsNames (post-process)
707
+ ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modName });
708
+ }
709
+ }
710
+ }
711
+ }
712
+ else if (RAILS_ASSOCIATION_METHODS.has(methodName)) {
713
+ // has_many :comments → Comment model
714
+ const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
715
+ const firstArg = args?.namedChildren[0];
716
+ if (firstArg?.type === 'simple_symbol') {
717
+ const assocName = firstArg.text.replace(/^:/, '');
718
+ const modelName = associationToModel(assocName);
719
+ ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modelName });
720
+ ctx.associationTargets.push(modelName);
721
+ }
722
+ }
723
+ else if (RAILS_CALLBACK_METHODS.has(methodName)) {
724
+ // before_action :method_name → calls edge within same class
725
+ const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
726
+ const firstArg = args?.namedChildren[0];
727
+ if (firstArg?.type === 'simple_symbol') {
728
+ const refMethod = firstArg.text.replace(/^:/, '');
729
+ ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: refMethod });
730
+ }
731
+ }
732
+ else if (RAILS_DSL_METHODS.has(methodName)) {
733
+ // attr_accessor, delegate, scope, enum, etc. → emit method symbols
734
+ extractDslMethods(node, methodName, parentName, ctx, symbols);
735
+ }
736
+ break;
737
+ }
738
+ // Handle bare method calls at class scope (include, has_many, etc.)
739
+ case 'command': {
740
+ if (!parentName)
741
+ return;
742
+ const methodNode = node.namedChildren[0];
743
+ if (!methodNode || methodNode.type !== 'identifier')
744
+ return;
745
+ const methodName = methodNode.text;
746
+ if (methodName === 'include' || methodName === 'extend' || methodName === 'prepend') {
747
+ const args = node.namedChildren.slice(1);
748
+ for (const arg of args) {
749
+ const modName = getConstantName(arg);
750
+ if (modName) {
751
+ ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modName });
752
+ }
753
+ }
754
+ }
755
+ else if (RAILS_ASSOCIATION_METHODS.has(methodName)) {
756
+ const firstArg = node.namedChildren[1];
757
+ if (firstArg?.type === 'simple_symbol') {
758
+ const assocName = firstArg.text.replace(/^:/, '');
759
+ const modelName = associationToModel(assocName);
760
+ ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: modelName });
761
+ ctx.associationTargets.push(modelName);
762
+ }
763
+ }
764
+ else if (RAILS_CALLBACK_METHODS.has(methodName)) {
765
+ const firstArg = node.namedChildren[1];
766
+ if (firstArg?.type === 'simple_symbol') {
767
+ const refMethod = firstArg.text.replace(/^:/, '');
768
+ ctx.extraCalls.push({ fromId: `${ctx.fileId}#${parentName}`, toName: refMethod });
769
+ }
770
+ }
771
+ else if (RAILS_DSL_METHODS.has(methodName)) {
772
+ extractDslMethodsFromCommand(methodName, parentName, node, ctx, symbols);
773
+ }
774
+ break;
775
+ }
776
+ }
777
+ }
778
+ // ---------------------------------------------------------------------------
779
+ // Constant and render collection (Rails autoload + render)
780
+ // ---------------------------------------------------------------------------
781
+ function isConstantDefinitionContext(node) {
782
+ // True if this constant is a definition (class/module name, assignment LHS), not a reference.
783
+ const p = node.parent;
784
+ if (!p)
785
+ return false;
786
+ // class Foo / module Foo — constant is first named child, don't resolve
787
+ if ((p.type === 'class' || p.type === 'module') && p.namedChildren[0] === node)
788
+ return true;
789
+ // Assignment LHS (FOO = 1) — don't resolve
790
+ if (p.type === 'assignment' && p.namedChildren[0] === node)
791
+ return true;
792
+ return false;
793
+ }
794
+ function shouldSkipConstantContext(node) {
795
+ // Skip if we're inside include/extend/prepend (already handled) or has_many/etc.
796
+ let p = node.parent;
797
+ while (p) {
798
+ if (p.type === 'call' || p.type === 'command') {
799
+ const mNode = p.type === 'call'
800
+ ? p.childForFieldName?.('method')
801
+ : p.namedChildren[0];
802
+ const m = mNode?.text;
803
+ if (m === 'include' || m === 'extend' || m === 'prepend')
804
+ return true;
805
+ }
806
+ // Don't walk past statement boundaries
807
+ if (p.type === 'body_statement' || p.type === 'program' || p.type === 'method' || p.type === 'class' || p.type === 'module')
808
+ break;
809
+ p = p.parent;
810
+ }
811
+ return false;
812
+ }
813
+ /**
814
+ * Collect constant references as ordered candidate groups, mirroring Ruby's
815
+ * lexical-nesting constant lookup. For a reference `R` seen inside `module A;
816
+ * module B`, Ruby tries `A::B::R`, `A::R`, then top-level `R` — so we emit
817
+ * `[A::B::R, A::R, R]` (innermost-first, bare last). The resolver tries each in
818
+ * order and takes the first that maps to a real file. This recovers references
819
+ * to namespaced files (e.g. bare `Invite` → `app/models/members/invite.rb`)
820
+ * that a literal-only lookup misses, and avoids wrong edges to top-level
821
+ * homonyms.
822
+ *
823
+ * `nesting` is the enclosing class/module FQCNs, innermost-first (= Module.nesting).
824
+ * Compact definitions (`class A::B`) don't add intermediate namespaces, matching Ruby.
825
+ */
826
+ function collectConstantReferences(root) {
827
+ const out = [];
828
+ const seen = new Set();
829
+ function emit(ref, nesting) {
830
+ const candidates = [...nesting.map(n => `${n}::${ref}`), ref];
831
+ const key = candidates.join('>');
832
+ if (seen.has(key))
833
+ return;
834
+ seen.add(key);
835
+ out.push(candidates);
836
+ }
837
+ function visit(node, nesting) {
838
+ if (node.type === 'class' || node.type === 'module') {
839
+ // Compute this scope's FQCN the same way extractNode does (parentName::name).
840
+ const nameNode = firstChildOfTypes(node, ['constant', 'scope_resolution']);
841
+ const text = nameNode?.text;
842
+ const newName = text ? (nesting.length ? `${nesting[0]}::${text}` : text) : nesting[0];
843
+ const newNesting = newName ? [newName, ...nesting] : nesting;
844
+ // Body resolves under the new nesting; the name/superclass resolve in the
845
+ // OUTER nesting (Ruby resolves `class Foo < Bar` and `Foo::X` before Foo is open).
846
+ const body = firstChildOfType(node, 'body_statement');
847
+ for (const c of node.namedChildren) {
848
+ visit(c, c === body ? newNesting : nesting);
849
+ }
850
+ return;
851
+ }
852
+ if (node.type === 'constant' || node.type === 'scope_resolution') {
853
+ if (!isConstantDefinitionContext(node) && !shouldSkipConstantContext(node)) {
854
+ const txt = node.text;
855
+ if (!isConstantStoplisted(txt))
856
+ emit(txt, nesting);
857
+ }
858
+ // Don't descend into scope_resolution children — we want the full path.
859
+ if (node.type === 'scope_resolution')
860
+ return;
861
+ }
862
+ for (const c of node.namedChildren)
863
+ visit(c, nesting);
864
+ }
865
+ visit(root, []);
866
+ return out;
867
+ }
868
+ const RUBY_MAX_STRING = 32000;
869
+ const RUBY_CHUNK_SIZE = 4096;
870
+ function parseContent(parser, content) {
871
+ if (content.length <= RUBY_MAX_STRING) {
872
+ return parser.parse(content);
873
+ }
874
+ return parser.parse((startIndex) => {
875
+ if (startIndex >= content.length)
876
+ return null;
877
+ return content.slice(startIndex, startIndex + RUBY_CHUNK_SIZE);
878
+ });
879
+ }
880
+ export function parseRubyContent(content, fileId) {
881
+ const parser = getParser();
882
+ let tree;
883
+ try {
884
+ tree = parseContent(parser, content);
885
+ }
886
+ catch (err) {
887
+ throw new Error(`Tree-sitter failed to parse Ruby (${content.length} chars): ${err}`);
888
+ }
889
+ const root = tree.rootNode;
890
+ const imports = [];
891
+ const exports = [];
892
+ const rawSymbols = [];
893
+ const ctx = {
894
+ fileId,
895
+ allSymbolNames: new Set(),
896
+ extraCalls: [],
897
+ associationTargets: [],
898
+ };
899
+ // First pass: collect all require/require_relative calls
900
+ function collectRequires(node) {
901
+ if (node.type === 'call') {
902
+ const methodNode = node.namedChildren.find((c) => c.type === 'identifier');
903
+ const isRelative = methodNode?.text === 'require_relative';
904
+ if (isRelative || methodNode?.text === 'require') {
905
+ const args = firstChildOfType(node, 'argument_list') ?? firstChildOfType(node, 'arguments');
906
+ const strNode = args
907
+ ? firstChildOfTypes(args, ['string', 'string_literal'])
908
+ : firstChildOfTypes(node, ['string', 'string_literal']);
909
+ if (strNode) {
910
+ const content = firstChildOfType(strNode, 'string_content');
911
+ const raw = content?.text ?? strNode.text.replace(/^['"]|['"]$/g, '');
912
+ if (raw)
913
+ imports.push(normalizeRequire(raw, isRelative));
914
+ }
915
+ }
916
+ }
917
+ else if (node.type === 'command') {
918
+ const methodNode = node.namedChildren[0];
919
+ const isRelative = methodNode?.text === 'require_relative';
920
+ if (methodNode?.type === 'identifier' && (isRelative || methodNode.text === 'require')) {
921
+ const strNode = node.namedChildren.find((c) => c.type === 'string' || c.type === 'string_literal');
922
+ if (strNode) {
923
+ const content = firstChildOfType(strNode, 'string_content');
924
+ const raw = content?.text ?? strNode.text.replace(/^['"]|['"]$/g, '');
925
+ if (raw)
926
+ imports.push(normalizeRequire(raw, isRelative));
927
+ }
928
+ }
929
+ }
930
+ for (const child of node.namedChildren) {
931
+ collectRequires(child);
932
+ }
933
+ }
934
+ function normalizeRequire(raw, isRelative) {
935
+ if (isRelative && !raw.startsWith('.') && !raw.startsWith('/')) {
936
+ return './' + raw;
937
+ }
938
+ return raw;
939
+ }
940
+ collectRequires(root);
941
+ // Second pass: extract symbols from top-level nodes
942
+ for (const node of root.namedChildren) {
943
+ extractNode(node, ctx, null, rawSymbols);
944
+ }
945
+ // Build exports from public symbols
946
+ for (const sym of rawSymbols) {
947
+ if (sym.isExported && sym.kind !== 'method') {
948
+ exports.push(sym.name);
949
+ }
950
+ }
951
+ // Apply extra calls (include/extend/prepend → implementsNames, Rails associations → calls)
952
+ const symbolById = new Map(rawSymbols.map(s => [s.id, s]));
953
+ for (const { fromId, toName } of ctx.extraCalls) {
954
+ const sym = symbolById.get(fromId);
955
+ if (sym) {
956
+ // For include/extend/prepend: add to implementsNames
957
+ // For Rails associations/callbacks: add to calls
958
+ sym.implementsNames.push(toName);
959
+ }
960
+ }
961
+ // For Rails models (files under app/models/), convert association names to file-level imports
962
+ if (fileId.includes('app/models/')) {
963
+ for (const modelName of ctx.associationTargets) {
964
+ const modelSnake = modelName.replace(/([A-Z])/g, '_$1').slice(1).toLowerCase();
965
+ const importPath = `./${modelSnake}`;
966
+ if (!imports.includes(importPath)) {
967
+ imports.push(importPath);
968
+ }
969
+ }
970
+ }
971
+ // Extract routes.rb controller imports
972
+ if (fileId.endsWith('config/routes.rb')) {
973
+ const routeImports = extractRoutesImports(root, fileId);
974
+ for (const imp of routeImports) {
975
+ if (!imports.includes(imp)) {
976
+ imports.push(imp);
977
+ }
978
+ }
979
+ }
980
+ // Collect Rails autoload constant references
981
+ const constantReferences = collectConstantReferences(root);
982
+ // Resolve intra-file calls: replace plain names with full IDs
983
+ const symbolIdByName = new Map(rawSymbols.map(s => [s.name, s.id]));
984
+ const resolvedSymbols = rawSymbols.map(sym => ({
985
+ ...sym,
986
+ calls: sym.calls.map(c => ({ name: symbolIdByName.get(c.name) ?? c.name, line: c.line })),
987
+ }));
988
+ return {
989
+ imports: [...new Set(imports)],
990
+ exports: [...new Set(exports)],
991
+ hasDefaultExport: false,
992
+ symbols: resolvedSymbols,
993
+ constantReferences: constantReferences.length > 0 ? constantReferences : undefined,
994
+ };
995
+ }
996
+ //# sourceMappingURL=ruby.js.map