@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,875 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { extractContentTokens, deriveRelatedFiles, deriveNameEchoFiles, RELATED_FILES_CAP, } from '../indexer/content-tokens.js';
3
+ import { buildRichPage } from './find.js';
4
+ // For a target file, find callers of its exported symbols via symbolEdges.
5
+ // Returns up to CALLERS_CAP unique caller files, each with up to a few example
6
+ // caller-symbol/line pairs.
7
+ const CALLERS_CAP = 12;
8
+ const CALL_LINES_PER_CALLER_FILE = 3;
9
+ function buildCallersForFile(fileId, file, index) {
10
+ // Build a map: exported symbolId → list of incoming calls (caller symbolId + line)
11
+ const exportedIds = new Set(file.symbols.filter(s => s.isExported).map(s => s.id));
12
+ if (exportedIds.size === 0)
13
+ return [];
14
+ // exportedSymbolId → caller fileId → [caller info]
15
+ const callsTo = new Map();
16
+ // symInfo for resolving symbolIds → fileId+name. Build lazily on first hit.
17
+ let symInfo = null;
18
+ function info() {
19
+ if (!symInfo)
20
+ symInfo = buildSymbolInfoMap(index);
21
+ return symInfo;
22
+ }
23
+ for (const edge of index.symbolEdges) {
24
+ if (edge.type !== 'calls')
25
+ continue;
26
+ if (!exportedIds.has(edge.to))
27
+ continue;
28
+ const fromInfo = info().get(edge.from);
29
+ // Skip self-references inside the target file
30
+ if (fromInfo?.file === file.relativePath)
31
+ continue;
32
+ const callerFile = fromInfo?.file;
33
+ const callerName = fromInfo?.name ?? edge.from;
34
+ if (!callerFile)
35
+ continue;
36
+ if (!callsTo.has(edge.to))
37
+ callsTo.set(edge.to, new Map());
38
+ const perFile = callsTo.get(edge.to);
39
+ if (!perFile.has(callerFile))
40
+ perFile.set(callerFile, []);
41
+ perFile.get(callerFile).push({ symbolName: callerName, line: edge.line });
42
+ }
43
+ const out = [];
44
+ for (const sym of file.symbols) {
45
+ if (!sym.isExported)
46
+ continue;
47
+ const perFile = callsTo.get(sym.id);
48
+ if (!perFile || perFile.size === 0)
49
+ continue;
50
+ const callers = [];
51
+ let truncated = 0;
52
+ for (const [callerFile, callsList] of perFile) {
53
+ if (callers.length >= CALLERS_CAP) {
54
+ truncated += 1;
55
+ continue;
56
+ }
57
+ const samples = callsList.slice(0, CALL_LINES_PER_CALLER_FILE).map(c => {
58
+ if (typeof c.line === 'number' && c.line > 0) {
59
+ return `${callerFile}:${c.line} (${c.symbolName})`;
60
+ }
61
+ return `${callerFile} (${c.symbolName})`;
62
+ });
63
+ const extra = callsList.length - samples.length;
64
+ callers.push(samples.join('; ') + (extra > 0 ? ` +${extra} more` : ''));
65
+ }
66
+ if (truncated > 0)
67
+ callers.push(`[+${truncated} more caller files]`);
68
+ out.push({ exportedSymbol: sym.name, callers });
69
+ }
70
+ return out;
71
+ }
72
+ // ============================================================================
73
+ // find — shares ONE engine across transports: buildRichPage. The MCP `find`
74
+ // tool and the CLI `find`/`gs index` reader both call it. Same query in, same
75
+ // page out, regardless of transport. Everything heavy (grep-recall + AST
76
+ // precision against the live root) lives in find.ts; this is a thin shim.
77
+ // ============================================================================
78
+ export async function handleFind(index, params) {
79
+ const query = (params.query ?? params.domain_filter ?? "").trim();
80
+ if (!query) {
81
+ return {
82
+ error: "query is required",
83
+ __rawText: "Error: missing `query`. Pass the salient identifiers from the task, " +
84
+ "e.g. \"ResourceInstance principaluser editable\".",
85
+ };
86
+ }
87
+ return { __rawText: await buildRichPage(index, index.rootDir, query) };
88
+ }
89
+ const IMPORTERS_LIST_CAP = 20;
90
+ const BODY_REF_IMPORTERS_CAP = 8;
91
+ // For huge files, show top-K symbols sorted by caller count first; the rest
92
+ // collapse into a tail. Avoids the 60-class wall-of-text problem.
93
+ const SYMBOLS_TOP_K = 15;
94
+ const SYMBOLS_HUGE_FILE_THRESHOLD = 20;
95
+ // Inline single caller when symbol has only 1; switch to newline-per-caller
96
+ // once a symbol has 2+ callers (otherwise the line balloons past 500 chars).
97
+ const CALLER_INLINE_MAX = 1;
98
+ function isTestPath(p) {
99
+ return p.startsWith('tests/') || p.startsWith('test/')
100
+ || p.includes('/tests/') || p.includes('/test/')
101
+ || /_tests?\.[a-z]+$/.test(p)
102
+ || /\.tests?\.[a-z]+$/.test(p);
103
+ }
104
+ export function handleGetStructure(index, params) {
105
+ if (!params.file_path) {
106
+ return { error: 'file_path is required' };
107
+ }
108
+ const fileEntry = findFileByPath(index, params.file_path);
109
+ if (!fileEntry) {
110
+ return { error: `File not found: ${params.file_path}` };
111
+ }
112
+ const [fileId, file] = fileEntry;
113
+ // Grade-2 symbol-body delivery: `--symbol name1,name2` slices the named
114
+ // method bodies straight from the indexed [startLine,endLine] range so the
115
+ // agent gets them in ONE call instead of windowing a god-file at guessed
116
+ // offsets (q22 read graph.py 8× hunting serialize/restore_state). Each slice
117
+ // is followed by 1-line caller/callee POINTERS (not bodies) so the next hop
118
+ // is one directed call away. This is the lever; grade-1 (the matched-symbol
119
+ // line ranges in `find`) only pointed the agent at the offsets.
120
+ if (params.symbol && params.symbol.trim()) {
121
+ return renderSymbolSlice(index, fileId, file, params.symbol);
122
+ }
123
+ // Validate `view`: an invalid value (stale client, typo, or a hallucinated
124
+ // name like "outline") would otherwise silently disable EVERY section and
125
+ // return a header-only response, pushing the agent to grep. Fail open —
126
+ // coerce to 'full' and surface a loud warning so the agent self-corrects.
127
+ const VALID_VIEWS = ['full', 'symbols', 'imports', 'importers', 'callers'];
128
+ const requestedView = params.view;
129
+ const viewIsInvalid = requestedView !== undefined && !VALID_VIEWS.includes(requestedView);
130
+ const view = viewIsInvalid ? 'full' : (requestedView ?? 'full');
131
+ const matchPredicate = compileMatchPredicate(params.match);
132
+ const wantSymbols = view === 'full' || view === 'symbols';
133
+ const wantImports = view === 'full' || view === 'imports';
134
+ const wantImporters = view === 'full' || view === 'importers';
135
+ const wantInlineCallers = view === 'full';
136
+ const wantExpandedCallers = view === 'callers';
137
+ // Pre-compute callers once if any caller view is on
138
+ let callersByName = null;
139
+ if (wantInlineCallers || wantExpandedCallers) {
140
+ const perSym = buildCallersForFile(fileId, file, index);
141
+ callersByName = new Map();
142
+ for (const entry of perSym) {
143
+ callersByName.set(entry.exportedSymbol, entry.callers);
144
+ }
145
+ }
146
+ let symbolsOut = [];
147
+ let totalSymbolCount = 0;
148
+ let filteredSymbolCount = 0;
149
+ let truncatedSymbolCount = 0;
150
+ let matchMissedSymbols = false;
151
+ if (wantSymbols) {
152
+ const baseFiltered = file.symbols
153
+ .filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method')
154
+ .slice()
155
+ .sort((a, b) => a.startLine - b.startLine);
156
+ totalSymbolCount = baseFiltered.length;
157
+ let filtered = matchPredicate
158
+ ? baseFiltered.filter(s => matchPredicate(s.name))
159
+ : baseFiltered;
160
+ // Match-miss auto-fallback: a "0 symbols match" result's only possible
161
+ // follow-up is re-calling without the filter (a measured wasted call).
162
+ // Return the full view instead, flagged via matchMissedSymbols.
163
+ if (matchPredicate && filtered.length === 0 && baseFiltered.length > 0) {
164
+ // The fix: before falling back to the full symbol MENU (which the agent
165
+ // ignores, then re-reads the whole file — jmri Q1), try mapping the match
166
+ // terms to their enclosing method bodies. Same machinery the `--symbol`
167
+ // miss uses. Returns null (→ menu) when the toggle is off or nothing hits.
168
+ if (params.match) {
169
+ try {
170
+ const allLines = readFileSync(file.path, 'utf-8').split('\n');
171
+ const methodMatch = renderEnclosingMethodMatch(file, params.match, allLines);
172
+ if (methodMatch)
173
+ return methodMatch;
174
+ }
175
+ catch { /* unreadable — fall through to the menu */ }
176
+ }
177
+ filtered = baseFiltered;
178
+ matchMissedSymbols = true;
179
+ }
180
+ filteredSymbolCount = filtered.length;
181
+ // Compute display names + parent class indent (preserves source-order
182
+ // tree structure before any reorder).
183
+ let lastClassName = null;
184
+ const annotated = filtered.map(s => {
185
+ let displayName = s.name;
186
+ let indent = '';
187
+ if (lastClassName && s.name.startsWith(lastClassName + '.')) {
188
+ displayName = s.name.slice(lastClassName.length + 1);
189
+ indent = ' ';
190
+ }
191
+ else if (s.kind === 'class') {
192
+ lastClassName = s.name;
193
+ }
194
+ const callers = callersByName?.get(s.name) ?? [];
195
+ return {
196
+ name: s.name,
197
+ displayName,
198
+ indent,
199
+ kind: s.kind,
200
+ startLine: s.startLine,
201
+ endLine: s.endLine,
202
+ extendsName: s.extendsName,
203
+ implementsNames: s.implementsNames,
204
+ callers,
205
+ callerFileCount: callers.length,
206
+ };
207
+ });
208
+ // For full view of huge files (no match filter), reorder by caller count
209
+ // desc so most-used symbols surface first; then truncate to top-K with a
210
+ // summary tail. With a match filter we honour the filter exactly — agent
211
+ // is steering, not browsing.
212
+ if (view === 'full' &&
213
+ (!matchPredicate || matchMissedSymbols) &&
214
+ annotated.length > SYMBOLS_HUGE_FILE_THRESHOLD) {
215
+ const sorted = annotated.slice().sort((a, b) => {
216
+ if (b.callerFileCount !== a.callerFileCount) {
217
+ return b.callerFileCount - a.callerFileCount;
218
+ }
219
+ return a.startLine - b.startLine;
220
+ });
221
+ // After reorder, parent-class indent is no longer meaningful (siblings
222
+ // are scattered), so show the full qualified name and drop indent.
223
+ symbolsOut = sorted.slice(0, SYMBOLS_TOP_K).map(s => ({
224
+ ...s,
225
+ displayName: s.name,
226
+ indent: '',
227
+ }));
228
+ truncatedSymbolCount = sorted.length - symbolsOut.length;
229
+ }
230
+ else {
231
+ symbolsOut = annotated;
232
+ }
233
+ }
234
+ // Build imports list
235
+ let importsOut = [];
236
+ let totalImportCount = 0;
237
+ if (wantImports) {
238
+ const internalImports = [...new Set(index.edges
239
+ .filter(e => e.from === fileId)
240
+ .map(e => index.files.get(e.to)?.relativePath)
241
+ .filter((p) => !!p))];
242
+ totalImportCount = internalImports.length;
243
+ importsOut = matchPredicate
244
+ ? internalImports.filter(p => matchPredicate(p))
245
+ : internalImports;
246
+ }
247
+ // Build importers list
248
+ let importersShown = [];
249
+ let importersFiltered = 0;
250
+ let importersExtra = 0;
251
+ let totalImporterCount = 0;
252
+ let bodyRefImporters = [];
253
+ let bodyRefTotal = 0;
254
+ if (wantImporters) {
255
+ const allImporters = buildAllImporters(fileId, index);
256
+ totalImporterCount = allImporters.length;
257
+ const filteredImporters = matchPredicate
258
+ ? allImporters.filter(p => matchPredicate(p))
259
+ : allImporters;
260
+ importersFiltered = filteredImporters.length;
261
+ importersShown = filteredImporters.slice(0, IMPORTERS_LIST_CAP);
262
+ importersExtra = filteredImporters.length - importersShown.length;
263
+ // Body-reference filter: with `match`, the filename filter alone hides the
264
+ // files an agent actually wants — files that *reference* the matched
265
+ // symbol but don't carry it in their path (q16: admin.py registers
266
+ // models.SpatialView via attribute access; not a call edge, filename
267
+ // doesn't match → invisible, and the agent reconstructed this set with 25
268
+ // greps). Scan is REPO-WIDE, not importer-scoped: the cross-language
269
+ // use-sites this section exists for (a JS file referencing a Python
270
+ // symbol) import nothing from the target file, so an importer-only scan
271
+ // misses them — and only a repo-wide scan makes the rendered
272
+ // exhaustiveness claim true (q16 run-3: the agent held "body-refs =
273
+ // admin.py + graph.py" at call 11 and still spent 20 calls hunting a JS
274
+ // frontend that doesn't exist, because nothing said the list was
275
+ // complete). Shape-gated tokens only, so single-word terms find nothing —
276
+ // the section is omitted, never rendered as a misleading "0".
277
+ if (matchPredicate) {
278
+ const alreadyShown = new Set(importersShown);
279
+ for (const [p, f] of index.files) {
280
+ if (p === fileId || alreadyShown.has(p))
281
+ continue;
282
+ const toks = f.contentTokens;
283
+ if (!toks)
284
+ continue;
285
+ for (const k of Object.keys(toks)) {
286
+ if (matchPredicate(k)) {
287
+ bodyRefImporters.push(p);
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ bodyRefTotal = bodyRefImporters.length;
293
+ // Source files first — tests are verification, not mechanism.
294
+ bodyRefImporters = [
295
+ ...bodyRefImporters.filter(p => !isTestPath(p)),
296
+ ...bodyRefImporters.filter(isTestPath),
297
+ ].slice(0, BODY_REF_IMPORTERS_CAP);
298
+ }
299
+ }
300
+ // Related files via the content-token channel (full view only). When the
301
+ // agent passed `match`, scope the source tokens to the matched symbols'
302
+ // line ranges — the match param is the agent's stated intent, and the echo
303
+ // lands at the attend-moment (the call right before the region gets Read).
304
+ // Dedupe is against what THIS payload renders (importsOut/importersShown),
305
+ // not the raw edge set — a god-file's truncated importer list can hide a
306
+ // file that the edge set technically contains.
307
+ let relatedOut = [];
308
+ if (view === 'full') {
309
+ const excludeIds = new Set([fileId, ...importsOut, ...importersShown, ...bodyRefImporters]);
310
+ let sourceTokens = file.contentTokens;
311
+ if (matchPredicate && !matchMissedSymbols && symbolsOut.length > 0 && index.contentTokenPostings.size > 0) {
312
+ try {
313
+ const allLines = readFileSync(file.path, 'utf-8').split('\n');
314
+ const parts = [];
315
+ let taken = 0;
316
+ for (const s of symbolsOut) {
317
+ parts.push(allLines.slice(s.startLine - 1, s.endLine).join('\n'));
318
+ taken += s.endLine - s.startLine + 1;
319
+ if (taken > 3000)
320
+ break;
321
+ }
322
+ sourceTokens = extractContentTokens(parts.join('\n'), fileId) ?? sourceTokens;
323
+ }
324
+ catch { /* unreadable — fall back to file-level tokens */ }
325
+ }
326
+ if (sourceTokens && index.contentTokenPostings.size > 0) {
327
+ relatedOut = deriveRelatedFiles(fileId, sourceTokens, index, excludeIds);
328
+ }
329
+ // Name-echo: filenames matching the agent's match terms (df-gated,
330
+ // separator-insensitive). Regex specs are skipped — terms only.
331
+ if (params.match && relatedOut.length < RELATED_FILES_CAP) {
332
+ const spec = params.match.trim();
333
+ if (!(spec.length >= 2 && spec.startsWith('/') && spec.endsWith('/'))) {
334
+ const terms = spec.split('|').map(s => s.trim()).filter(s => s.length > 0);
335
+ const echoExclude = new Set([...excludeIds, ...relatedOut.map(r => r.fileId)]);
336
+ relatedOut = [
337
+ ...relatedOut,
338
+ ...deriveNameEchoFiles(terms, index, echoExclude),
339
+ ].slice(0, RELATED_FILES_CAP);
340
+ }
341
+ }
342
+ }
343
+ // Render text.
344
+ const lines = [`${file.relativePath} (${file.lineCount} lines, importedBy: ${file.importedByCount})`];
345
+ if (viewIsInvalid) {
346
+ lines.push(`[note: view "${String(requestedView)}" is not valid — showing full view. Valid views: ${VALID_VIEWS.join(', ')}.]`);
347
+ }
348
+ if (wantSymbols) {
349
+ if (symbolsOut.length > 0) {
350
+ lines.push('');
351
+ if (matchMissedSymbols) {
352
+ lines.push(`[0 of ${totalSymbolCount} symbols match "${params.match}" — showing all symbols instead:]`);
353
+ }
354
+ lines.push('Symbols:');
355
+ for (const s of symbolsOut) {
356
+ const range = s.startLine === s.endLine ? `[L${s.startLine}]` : `[L${s.startLine}-${s.endLine}]`;
357
+ let line = `${s.indent}${s.kind} ${s.displayName} ${range}`;
358
+ if (s.extendsName)
359
+ line += ` extends ${s.extendsName}`;
360
+ if (s.implementsNames.length > 0)
361
+ line += ` implements ${s.implementsNames.join(', ')}`;
362
+ // Inline single caller; newline-per-caller block when ≥2.
363
+ if (wantInlineCallers && s.callers.length > 0) {
364
+ if (s.callers.length <= CALLER_INLINE_MAX) {
365
+ line += ` ← ${s.callers[0]}`;
366
+ lines.push(line);
367
+ }
368
+ else {
369
+ lines.push(line);
370
+ for (const c of s.callers) {
371
+ lines.push(`${s.indent} ← ${c}`);
372
+ }
373
+ }
374
+ }
375
+ else {
376
+ lines.push(line);
377
+ }
378
+ }
379
+ if (truncatedSymbolCount > 0) {
380
+ lines.push(`[+${truncatedSymbolCount} more symbols — pass \`match\` to filter, or \`view: "symbols"\` for full list without callers]`);
381
+ }
382
+ }
383
+ else if (matchPredicate && totalSymbolCount > 0) {
384
+ lines.push('');
385
+ lines.push(`Symbols: 0 of ${totalSymbolCount} match "${params.match}".`);
386
+ }
387
+ }
388
+ // Expanded callers section (view: 'callers' only)
389
+ if (wantExpandedCallers && callersByName) {
390
+ let totalSyms = 0;
391
+ let shownSyms = 0;
392
+ lines.push('');
393
+ lines.push('Callers (per exported symbol):');
394
+ for (const [symName, callers] of callersByName) {
395
+ totalSyms++;
396
+ if (matchPredicate && !matchPredicate(symName))
397
+ continue;
398
+ shownSyms++;
399
+ lines.push(` ${symName}:`);
400
+ for (const c of callers)
401
+ lines.push(` ${c}`);
402
+ }
403
+ if (totalSyms === 0) {
404
+ lines.push(' (no symbol-level callers indexed — no exported symbols, no member-expression call sites, or no callers exist)');
405
+ }
406
+ else if (matchPredicate && shownSyms === 0) {
407
+ lines.push(` (0 of ${totalSyms} symbols with callers match "${params.match}")`);
408
+ }
409
+ }
410
+ // Imports
411
+ if (wantImports) {
412
+ const IMPORTS_LIST_THRESHOLD = 15;
413
+ if (importsOut.length > 0) {
414
+ lines.push('');
415
+ if (importsOut.length <= IMPORTS_LIST_THRESHOLD || matchPredicate) {
416
+ lines.push('Imports:');
417
+ for (const p of importsOut)
418
+ lines.push(p);
419
+ }
420
+ else {
421
+ lines.push(`Imports: ${importsOut.length} internal files — pass \`match\` to scope this list (substring or /regex/).`);
422
+ }
423
+ }
424
+ else if (matchPredicate && totalImportCount > 0) {
425
+ lines.push('');
426
+ lines.push(`Imports: 0 of ${totalImportCount} match "${params.match}".`);
427
+ }
428
+ }
429
+ // Importers — section by test/source when both groups present so the agent
430
+ // has a labeled attention slot for test files (they're verification, not
431
+ // peer call sites; agent triages them differently if labeled).
432
+ if (wantImporters) {
433
+ if (importersFiltered > 0) {
434
+ const testImporters = importersShown.filter(isTestPath);
435
+ const sourceImporters = importersShown.filter(p => !isTestPath(p));
436
+ lines.push('');
437
+ lines.push(`Importers (${importersFiltered}${importersExtra > 0 ? `, showing ${importersShown.length}` : ''}):`);
438
+ if (testImporters.length > 0 && sourceImporters.length > 0) {
439
+ lines.push(` Source (${sourceImporters.length}):`);
440
+ for (const p of sourceImporters)
441
+ lines.push(` ${p}`);
442
+ lines.push(` Tests (${testImporters.length}):`);
443
+ for (const p of testImporters)
444
+ lines.push(` ${p}`);
445
+ }
446
+ else {
447
+ for (const p of importersShown)
448
+ lines.push(p);
449
+ }
450
+ if (importersExtra > 0)
451
+ lines.push(`[+${importersExtra} more — narrow with \`match\`]`);
452
+ }
453
+ else if (matchPredicate && totalImporterCount > 0) {
454
+ lines.push('');
455
+ lines.push(`Importers: 0 of ${totalImporterCount} match "${params.match}" by filename.`);
456
+ }
457
+ // Body references: the grep-replacement answer to "who uses <match>" —
458
+ // files whose CONTENT references the matched term even though their
459
+ // filename doesn't. Repo-wide scan, so the closing exhaustiveness line is
460
+ // a true claim: absence here = no other indexed file names the term.
461
+ if (bodyRefImporters.length > 0) {
462
+ lines.push('');
463
+ lines.push(`Files referencing "${params.match}" in content (${bodyRefTotal}${bodyRefTotal > bodyRefImporters.length ? `, showing ${bodyRefImporters.length}` : ''}) — use-sites the lists above miss:`);
464
+ for (const p of bodyRefImporters)
465
+ lines.push(` ${p}`);
466
+ if (bodyRefTotal > bodyRefImporters.length) {
467
+ lines.push(` The count (${bodyRefTotal}) is exhaustive over indexed file content — exactly that many files reference "${params.match}" as a named identifier; the list is truncated, narrow \`match\` to see the rest. Do not grep to re-verify.`);
468
+ }
469
+ else {
470
+ lines.push(` This list is exhaustive over indexed file content: no other file references "${params.match}" as a named identifier. Do not grep to re-verify, and do not hunt for use-sites in other subsystems.`);
471
+ }
472
+ }
473
+ }
474
+ // Related (content-token channel)
475
+ if (relatedOut.length > 0) {
476
+ lines.push('');
477
+ lines.push('Related (shares rare tokens, no import edge — name-reference relations the import graph cannot see):');
478
+ for (const r of relatedOut) {
479
+ if (r.viaName) {
480
+ lines.push(` ${r.fileId} — filename matches "${r.viaName}"`);
481
+ }
482
+ else {
483
+ const ids = [r.fileId, ...(r.alsoFileIds ?? [])];
484
+ const names = ids.slice(0, 2).join(', ') + (ids.length > 2 ? ` +${ids.length - 2}` : '');
485
+ const shown = r.tokens.slice(0, 3).map(t => `\`${t}\``).join(', ');
486
+ const extra = r.tokens.length - 3;
487
+ lines.push(` ${names} — shares ${shown}${extra > 0 ? ` +${extra}` : ''}`);
488
+ }
489
+ }
490
+ }
491
+ // Next-step pointer (only when there's something useful to say)
492
+ const nextHint = computeNextHint({
493
+ hasMatch: !!matchPredicate,
494
+ wantSymbols,
495
+ filteredSymbolCount,
496
+ totalSymbolCount,
497
+ });
498
+ if (nextHint) {
499
+ lines.push('');
500
+ lines.push(nextHint);
501
+ }
502
+ return { __rawText: lines.join('\n') };
503
+ }
504
+ // Grade-2 caps. Bodies are the expensive payload, so bound them; pointers are
505
+ // cheap so they stay generous.
506
+ const SLICE_SYMBOL_CAP = 4; // distinct symbols sliced in one call
507
+ const SLICE_LINE_BUDGET = 450; // total body lines across all slices
508
+ const SLICE_CALLEE_CAP = 10; // callee pointers per symbol
509
+ // Resolve requested symbol name(s) → SymbolNodes in `file`, tiered so a bare
510
+ // last-segment like `serialize` lands on `Graph.serialize` and never on the
511
+ // substring lookalike `serialized_graph`. Tiers, first non-empty wins per name:
512
+ // 1. exact full name (`Graph.serialize`) 2. exact last segment (`serialize`)
513
+ // 3. case-insensitive substring (last resort, e.g. a typo or partial recall)
514
+ function resolveSliceSymbols(file, spec) {
515
+ const names = spec.split(/[,|]/).map(s => s.trim()).filter(Boolean);
516
+ const picked = new Map(); // by symbol id, dedupe
517
+ for (const q of names) {
518
+ const ql = q.toLowerCase();
519
+ const exact = file.symbols.filter(s => s.name.toLowerCase() === ql);
520
+ const lastSeg = exact.length ? exact
521
+ : file.symbols.filter(s => s.name.toLowerCase().split('.').pop() === ql);
522
+ const tier = lastSeg.length ? lastSeg
523
+ : file.symbols.filter(s => s.name.toLowerCase().includes(ql));
524
+ for (const s of tier)
525
+ picked.set(s.id, s);
526
+ }
527
+ return [...picked.values()].sort((a, b) => a.startLine - b.startLine);
528
+ }
529
+ // In-tool grep fallback for `--symbol` when no declared symbol matches: locate
530
+ // the token(s) in the file body and return the matching line regions with
531
+ // context, so the agent never shells out to grep. Caps keep it byte-light.
532
+ const CONTENT_CONTEXT = 2; // context lines each side of a match
533
+ const CONTENT_REGION_CAP = 8; // distinct regions returned
534
+ const CONTENT_LINE_CAP = 70; // total lines returned
535
+ function renderContentMatch(file, spec, allLines) {
536
+ const terms = spec.split(/[,|]/).map(s => s.trim().toLowerCase()).filter(Boolean);
537
+ const hitLines = []; // 0-based
538
+ for (let i = 0; i < allLines.length; i++) {
539
+ const low = allLines[i].toLowerCase();
540
+ if (terms.some(t => low.includes(t)))
541
+ hitLines.push(i);
542
+ }
543
+ if (hitLines.length === 0) {
544
+ const avail = file.symbols
545
+ .filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method')
546
+ .slice(0, 25).map(s => s.name).join(', ');
547
+ return {
548
+ __rawText: `${file.relativePath}: no symbol named "${spec}", and the token does not appear in this file's content either.\n` +
549
+ (avail ? `Declared symbols here: ${avail}${file.symbols.length > 25 ? ', …' : ''}` : 'No top-level symbols indexed in this file.') +
550
+ '\nThe identifier is not in this file — check the file path, or the value may be injected at runtime from elsewhere.',
551
+ error: `no symbol or content match for "${spec}"`,
552
+ };
553
+ }
554
+ // Merge nearby hits into regions.
555
+ const regions = [];
556
+ for (const ln of hitLines) {
557
+ const start = Math.max(0, ln - CONTENT_CONTEXT);
558
+ const end = Math.min(allLines.length - 1, ln + CONTENT_CONTEXT);
559
+ const last = regions[regions.length - 1];
560
+ if (last && start <= last[1] + 1)
561
+ last[1] = Math.max(last[1], end);
562
+ else
563
+ regions.push([start, end]);
564
+ }
565
+ const lines = [
566
+ `${file.relativePath} (${file.lineCount} lines) — no declared symbol matches "${spec}"; showing ${hitLines.length} content match${hitLines.length === 1 ? '' : 'es'} (in-tool grep, so you don't have to):`,
567
+ ];
568
+ let shown = 0;
569
+ let r = 0;
570
+ for (const [start, end] of regions) {
571
+ if (r >= CONTENT_REGION_CAP || shown >= CONTENT_LINE_CAP) {
572
+ lines.push(` … [+${regions.length - r} more match regions — narrow \`--symbol\` or read the file]`);
573
+ break;
574
+ }
575
+ r++;
576
+ lines.push('');
577
+ for (let i = start; i <= end && shown < CONTENT_LINE_CAP; i++) {
578
+ lines.push(`${String(i + 1).padStart(5)} ${allLines[i]}`);
579
+ shown++;
580
+ }
581
+ }
582
+ lines.push('');
583
+ lines.push('These are content matches, not a declared symbol — the token has no static definition here (often runtime/template-injected). Grepping this file again returns these same lines; you already have them.');
584
+ // STOP-GUESSING menu: a wrong `--symbol` guess still produces content hits (the word appears
585
+ // somewhere in the body), which reads as "productive" and invites re-guessing OTHER names — the
586
+ // #1 wasted-call pattern (q21: 3 serial gs on one file guessing upload/unzip/write_zip_file…,
587
+ // none of which are methods). Surface the file's ACTUAL declared symbols so the next call picks a
588
+ // real one instead of guessing again. Same menu the no-hit branch already shows.
589
+ const menu = file.symbols
590
+ .filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method')
591
+ .slice(0, 25).map(s => s.name);
592
+ if (menu.length) {
593
+ lines.push('');
594
+ lines.push(`"${spec}" is NOT a declared symbol here. This file's methods are: ${menu.join(', ')}${file.symbols.length > 25 ? ', …' : ''}`);
595
+ lines.push(`These are the file's only declared symbols — a name that isn't listed has no body to slice. Is the name you need in this list? If yes, pass it; if not, the token is runtime/template text — Read the file rather than guessing another \`--symbol\`.`);
596
+ }
597
+ return { __rawText: lines.join('\n') };
598
+ }
599
+ // ── In-body grep → ENCLOSING METHOD (the gs-trace-cost fix) ─────────────────
600
+ // When neither a declared symbol (`--symbol`) nor a symbol-name filter
601
+ // (`--match`) hits, the agent's intent was almost always "show me the body that
602
+ // mentions these terms". The old answers were both wrong: `--symbol` miss dumped
603
+ // raw ±2-line regions; `--match` miss dumped the full symbol MENU (which the
604
+ // agent then ignored, re-reading ~950 lines — jmri Q1). This maps each body hit
605
+ // to its INNERMOST enclosing symbol via [startLine,endLine] ranges and returns
606
+ // those METHOD bodies, ranked by how many distinct terms each covers — the
607
+ // meaningful unit, not a line window and not a menu.
608
+ const METHOD_MATCH_SYMBOL_CAP = 4; // distinct enclosing methods sliced
609
+ const METHOD_MATCH_LINE_BUDGET = 240; // total body lines across all method slices
610
+ const METHOD_MATCH_ORPHAN_CAP = 6; // raw context lines for module-level hits
611
+ // Returns null when the spec is a /regex/ or no body line matches — caller then
612
+ // falls back to the existing menu / raw-region behavior.
613
+ function renderEnclosingMethodMatch(file, spec, allLines) {
614
+ const trimmed = spec.trim();
615
+ // Regex specs aren't word-splittable into terms; leave them to the old path.
616
+ if (trimmed.length >= 2 && trimmed.startsWith('/') && trimmed.endsWith('/'))
617
+ return null;
618
+ const terms = trimmed.split(/[\s,|]+/).map(s => s.trim().toLowerCase()).filter(Boolean);
619
+ if (terms.length === 0)
620
+ return null;
621
+ // hit line (1-based) → set of distinct terms found on it
622
+ const hitTermsByLine = new Map();
623
+ for (let i = 0; i < allLines.length; i++) {
624
+ const low = allLines[i].toLowerCase();
625
+ const matched = terms.filter(t => low.includes(t));
626
+ if (matched.length)
627
+ hitTermsByLine.set(i + 1, new Set(matched));
628
+ }
629
+ if (hitTermsByLine.size === 0)
630
+ return null;
631
+ const candidates = file.symbols.filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method');
632
+ const bySym = new Map();
633
+ const orphanLines = [];
634
+ for (const [line, matchedTerms] of hitTermsByLine) {
635
+ let best = null;
636
+ for (const s of candidates) {
637
+ if (s.startLine <= line && line <= s.endLine) {
638
+ if (!best || (s.endLine - s.startLine) < (best.endLine - best.startLine))
639
+ best = s;
640
+ }
641
+ }
642
+ if (!best) {
643
+ orphanLines.push(line);
644
+ continue;
645
+ }
646
+ let agg = bySym.get(best.id);
647
+ if (!agg) {
648
+ agg = { sym: best, terms: new Set() };
649
+ bySym.set(best.id, agg);
650
+ }
651
+ for (const t of matchedTerms)
652
+ agg.terms.add(t);
653
+ }
654
+ if (bySym.size === 0 && orphanLines.length === 0)
655
+ return null;
656
+ // Rank methods by distinct-term coverage desc, then source order.
657
+ const ranked = [...bySym.values()].sort((a, b) => b.terms.size !== a.terms.size ? b.terms.size - a.terms.size : a.sym.startLine - b.sym.startLine);
658
+ const lines = [
659
+ `${file.relativePath} (${file.lineCount} lines) — no declared symbol matches "${spec}"; ` +
660
+ `${hitTermsByLine.size} content match${hitTermsByLine.size === 1 ? '' : 'es'}` +
661
+ (ranked.length ? ` in ${ranked.length} method${ranked.length === 1 ? '' : 's'}` : '') +
662
+ ` (in-tool grep → method body, so you don't have to):`,
663
+ ];
664
+ let spent = 0;
665
+ let shownSyms = 0;
666
+ for (const agg of ranked) {
667
+ if (shownSyms >= METHOD_MATCH_SYMBOL_CAP || spent >= METHOD_MATCH_LINE_BUDGET) {
668
+ const rest = ranked.slice(shownSyms).map(a => a.sym.name).join(', ');
669
+ lines.push('');
670
+ lines.push(` … [+${ranked.length - shownSyms} more matching method${ranked.length - shownSyms === 1 ? '' : 's'}: ${rest} — re-call \`--symbol <name>\` for one]`);
671
+ break;
672
+ }
673
+ shownSyms++;
674
+ const s = agg.sym;
675
+ lines.push('');
676
+ lines.push(`━━ ${s.kind} ${s.name} [L${s.startLine}-${s.endLine}] — matches ${agg.terms.size}/${terms.length} terms: ${[...agg.terms].join(', ')} ━━`);
677
+ const want = s.endLine - s.startLine + 1;
678
+ const take = Math.min(want, Math.max(0, METHOD_MATCH_LINE_BUDGET - spent));
679
+ for (let i = s.startLine; i < s.startLine + take && i <= allLines.length; i++) {
680
+ lines.push(`${String(i).padStart(5)} ${allLines[i - 1]}`);
681
+ }
682
+ if (take < want)
683
+ lines.push(` … [+${want - take} more lines — re-call \`--symbol ${s.name}\` for the full body]`);
684
+ spent += take;
685
+ }
686
+ // Module-level hits (no enclosing method): a short raw-context tail so an
687
+ // import/constant/decorator match isn't silently dropped.
688
+ if (orphanLines.length > 0) {
689
+ lines.push('');
690
+ lines.push(`Also matched outside any method (module level):`);
691
+ let shown = 0;
692
+ for (const ln of orphanLines) {
693
+ if (shown >= METHOD_MATCH_ORPHAN_CAP) {
694
+ lines.push(` … [+${orphanLines.length - shown} more module-level match lines]`);
695
+ break;
696
+ }
697
+ lines.push(`${String(ln).padStart(5)} ${allLines[ln - 1]}`);
698
+ shown++;
699
+ }
700
+ }
701
+ lines.push('');
702
+ lines.push('These are the methods whose bodies contain the term(s); grepping this file returns the same lines you already have. Follow up with `--symbol <name>` for a full body.');
703
+ return { __rawText: lines.join('\n') };
704
+ }
705
+ function renderSymbolSlice(index, fileId, file, spec) {
706
+ const wanted = resolveSliceSymbols(file, spec);
707
+ let allLines;
708
+ try {
709
+ allLines = readFileSync(file.path, 'utf-8').split('\n');
710
+ }
711
+ catch (e) {
712
+ return { error: `could not read ${file.relativePath}: ${e}` };
713
+ }
714
+ // No declared symbol matched — fall back to a CONTENT match (in-tool grep):
715
+ // return the body lines where the requested token(s) live, with context. This
716
+ // is the answer for tokens that have no static symbol (runtime/template-
717
+ // injected data like `arches.termSearchTypes`, string keys, config values) —
718
+ // so the agent gets the body text from the SAME call instead of shelling out
719
+ // to grep and guessing spellings (q19's 5 empty greps).
720
+ if (wanted.length === 0) {
721
+ const methodMatch = renderEnclosingMethodMatch(file, spec, allLines);
722
+ return methodMatch ?? renderContentMatch(file, spec, allLines);
723
+ }
724
+ // Caller pointers: reuse the file-level builder (exported symbols only).
725
+ const callersByName = new Map();
726
+ for (const entry of buildCallersForFile(fileId, file, index)) {
727
+ callersByName.set(entry.exportedSymbol, entry.callers);
728
+ }
729
+ // Callee pointers: resolve this symbol's outgoing calls edges → target location.
730
+ const symInfo = buildSymbolInfoMap(index);
731
+ const locById = new Map();
732
+ for (const f of index.files.values()) {
733
+ for (const s of f.symbols)
734
+ locById.set(s.id, { rel: f.relativePath, start: s.startLine, end: s.endLine });
735
+ }
736
+ const calleesOf = (symId) => {
737
+ const seen = new Set();
738
+ const out = [];
739
+ for (const edge of index.symbolEdges) {
740
+ if (edge.type !== 'calls' || edge.from !== symId)
741
+ continue;
742
+ if (seen.has(edge.to))
743
+ continue;
744
+ seen.add(edge.to);
745
+ const meta = symInfo.get(edge.to);
746
+ if (!meta)
747
+ continue;
748
+ const loc = locById.get(edge.to);
749
+ out.push(loc
750
+ ? `${meta.name} → ${loc.rel} [L${loc.start}-${loc.end}]`
751
+ : `${meta.name} → ${meta.file}`);
752
+ if (out.length >= SLICE_CALLEE_CAP)
753
+ break;
754
+ }
755
+ return out;
756
+ };
757
+ const lines = [`${file.relativePath} (${file.lineCount} lines) — bodies for: ${wanted.map(s => s.name).join(', ')}`];
758
+ const sliced = wanted.slice(0, SLICE_SYMBOL_CAP);
759
+ if (wanted.length > sliced.length) {
760
+ lines.push(`[${wanted.length} symbols matched; slicing the first ${sliced.length} — narrow \`--symbol\` for the rest: ${wanted.slice(sliced.length).map(s => s.name).join(', ')}]`);
761
+ }
762
+ let spent = 0;
763
+ for (const s of sliced) {
764
+ lines.push('');
765
+ lines.push(`━━ ${s.kind} ${s.name} [L${s.startLine}-${s.endLine}]${s.extendsName ? ` extends ${s.extendsName}` : ''} ━━`);
766
+ const want = s.endLine - s.startLine + 1;
767
+ const room = Math.max(0, SLICE_LINE_BUDGET - spent);
768
+ if (room === 0) {
769
+ lines.push(` [line budget reached — re-call \`--symbol ${s.name}\` alone to read this body]`);
770
+ }
771
+ else {
772
+ const take = Math.min(want, room);
773
+ for (let i = s.startLine; i < s.startLine + take && i <= allLines.length; i++) {
774
+ lines.push(`${String(i).padStart(5)} ${allLines[i - 1]}`);
775
+ }
776
+ if (take < want)
777
+ lines.push(` … [+${want - take} more lines truncated by budget — re-call \`--symbol ${s.name}\` alone for the full body]`);
778
+ spent += take;
779
+ }
780
+ // Pointers (not bodies): who calls this, what this calls — each is one directed `--symbol` hop away.
781
+ const callers = callersByName.get(s.name) ?? [];
782
+ if (callers.length)
783
+ lines.push(` callers: ${callers.slice(0, 6).join(' · ')}`);
784
+ const callees = calleesOf(s.id);
785
+ if (callees.length)
786
+ lines.push(` calls: ${callees.join(' · ')}`);
787
+ if (!callers.length && !callees.length)
788
+ lines.push(' (no indexed callers/callees)');
789
+ }
790
+ lines.push('');
791
+ lines.push('Bodies delivered inline — no Read needed. Follow `calls:`/`callers:` pointers with another `gs <file> --symbol <name>` to hop without windowing.');
792
+ return { __rawText: lines.join('\n') };
793
+ }
794
+ function computeNextHint(opts) {
795
+ if (opts.hasMatch && opts.wantSymbols && opts.filteredSymbolCount === 0 && opts.totalSymbolCount > 0) {
796
+ return 'Next: relax or drop `match`; or call again with a narrower `view` (e.g. "imports", "importers").';
797
+ }
798
+ return '';
799
+ }
800
+ // Full importer list (inbound edges) — capped at IMPORTERS_LIST_CAP at display
801
+ // time, not here.
802
+ function buildAllImporters(fileId, index) {
803
+ const ids = index.inEdges.get(fileId) ?? [];
804
+ return ids
805
+ .map(id => index.files.get(id)?.relativePath)
806
+ .filter((p) => !!p)
807
+ .sort();
808
+ }
809
+ // Compile a `match` filter for GS. Wrapped in `/.../` → regex; `a|b` →
810
+ // OR of case-insensitive substrings; otherwise case-insensitive substring.
811
+ // Returns null when no filter is requested or the spec is empty.
812
+ function compileMatchPredicate(spec) {
813
+ if (!spec)
814
+ return null;
815
+ const trimmed = spec.trim();
816
+ if (!trimmed)
817
+ return null;
818
+ if (trimmed.length >= 2 && trimmed.startsWith('/') && trimmed.endsWith('/')) {
819
+ const body = trimmed.slice(1, -1);
820
+ try {
821
+ const re = new RegExp(body, 'i');
822
+ return (s) => re.test(s);
823
+ }
824
+ catch {
825
+ // Fall through to substring on malformed regex
826
+ }
827
+ }
828
+ if (trimmed.includes('|')) {
829
+ const alternatives = trimmed
830
+ .split('|')
831
+ .map(s => s.trim().toLowerCase())
832
+ .filter(s => s.length > 0);
833
+ if (alternatives.length > 1) {
834
+ return (s) => {
835
+ const lower = s.toLowerCase();
836
+ return alternatives.some(a => lower.includes(a));
837
+ };
838
+ }
839
+ }
840
+ const lower = trimmed.toLowerCase();
841
+ return (s) => s.toLowerCase().includes(lower);
842
+ }
843
+ // ============================================================================
844
+ // Helper: build a flat map of symbolId → { name, file, kind }
845
+ // ============================================================================
846
+ function buildSymbolInfoMap(index) {
847
+ const map = new Map();
848
+ for (const file of index.files.values()) {
849
+ for (const sym of file.symbols) {
850
+ map.set(sym.id, { name: sym.name, file: file.relativePath, kind: sym.kind });
851
+ }
852
+ }
853
+ return map;
854
+ }
855
+ // ============================================================================
856
+ // Helper: find file by path (exact relative, or suffix)
857
+ // ============================================================================
858
+ function findFileByPath(index, pathQuery) {
859
+ if (!pathQuery)
860
+ return null;
861
+ // Normalize
862
+ const normalized = pathQuery.replace(/\\/g, '/');
863
+ // Exact match first
864
+ if (index.files.has(normalized)) {
865
+ return [normalized, index.files.get(normalized)];
866
+ }
867
+ // Suffix match
868
+ for (const [id, file] of index.files) {
869
+ if (id.endsWith(normalized) || id.includes(normalized)) {
870
+ return [id, file];
871
+ }
872
+ }
873
+ return null;
874
+ }
875
+ //# sourceMappingURL=tools.js.map