scip-query 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/README.md +16 -43
  2. package/dist/chunk-2UELLEBI.js +1 -0
  3. package/dist/chunk-34JPTNRN.js +601 -0
  4. package/dist/{chunk-NDSQYIWT.js → chunk-3566TKJ5.js} +3 -3
  5. package/dist/{chunk-LB7OS35Q.js → chunk-4ACRRQC4.js} +8 -4
  6. package/dist/{chunk-3E2X7RIE.js → chunk-4BQFSNFI.js} +10 -6
  7. package/dist/{chunk-BP2ATLK2.js → chunk-6QSHLFSL.js} +4 -4
  8. package/dist/{chunk-5FGUEU7N.js → chunk-6WVR5K46.js} +18 -10
  9. package/dist/{chunk-XFXDXEUN.js → chunk-74RFWB5T.js} +2 -2
  10. package/dist/{chunk-MBVNHJVN.js → chunk-75RQSBTK.js} +2 -2
  11. package/dist/{chunk-YZAA4LYG.js → chunk-7HK5ZLOE.js} +30 -48
  12. package/dist/{chunk-T6ARFSBZ.js → chunk-7JFZSOJ7.js} +7 -7
  13. package/dist/{chunk-6SXADWLW.js → chunk-AKMBBKWV.js} +2 -2
  14. package/dist/{chunk-ZJRYBOEE.js → chunk-AMNISGYR.js} +5 -5
  15. package/dist/{chunk-CM454WL3.js → chunk-BFLULBEU.js} +3 -3
  16. package/dist/{chunk-Z73NYSBZ.js → chunk-CU62ZDHI.js} +2 -2
  17. package/dist/{chunk-TBP6BICL.js → chunk-DY4AFG2W.js} +13 -11
  18. package/dist/{chunk-2QZ23IBN.js → chunk-F7XU27LU.js} +4 -4
  19. package/dist/{chunk-KCBMVQL5.js → chunk-GPJVPT3U.js} +2 -2
  20. package/dist/{chunk-NUZ4OMU3.js → chunk-GU2H5QRN.js} +2 -2
  21. package/dist/{chunk-TSPZOMHC.js → chunk-H6WCPKCX.js} +6 -3
  22. package/dist/{chunk-KVSW5KYP.js → chunk-HDSRORNV.js} +4 -4
  23. package/dist/{chunk-LUSIFBXO.js → chunk-HMYJJ3HY.js} +9 -6
  24. package/dist/chunk-IJKLB2JW.js +69 -0
  25. package/dist/{chunk-6NBLIDF4.js → chunk-ITZ3DDOG.js} +2 -2
  26. package/dist/{chunk-GTILYBH6.js → chunk-IXPHLF6K.js} +6 -6
  27. package/dist/{chunk-BFSCMC22.js → chunk-KBOQX573.js} +3 -3
  28. package/dist/{chunk-FUHJCHS4.js → chunk-LLMPAG56.js} +95 -32
  29. package/dist/{chunk-FFSWWE5O.js → chunk-LTJC5ZQL.js} +3 -3
  30. package/dist/{chunk-LAWMH22O.js → chunk-M3NPW3FC.js} +2 -2
  31. package/dist/{chunk-VRUJH4BO.js → chunk-M4QGEKKD.js} +6 -28
  32. package/dist/{chunk-7OZPA5OO.js → chunk-MVH45PYK.js} +21 -41
  33. package/dist/chunk-N4C3H7LH.js +37 -0
  34. package/dist/chunk-NG5F43OU.js +200 -0
  35. package/dist/{chunk-6VJ6Q7IE.js → chunk-NVIIM34O.js} +4 -4
  36. package/dist/{chunk-GJFURBEW.js → chunk-ORINICIZ.js} +4 -4
  37. package/dist/{chunk-TDNNOR6D.js → chunk-PMJKOXOT.js} +7 -7
  38. package/dist/{chunk-QOV2R2WT.js → chunk-QIXNAB5K.js} +42 -2
  39. package/dist/{chunk-JKP5GH6T.js → chunk-R2I3M5B4.js} +2 -2
  40. package/dist/{chunk-36OMT7ZJ.js → chunk-R56FJU3E.js} +35 -14
  41. package/dist/{chunk-VZ7AMAFL.js → chunk-RFMT7UAZ.js} +3 -3
  42. package/dist/{chunk-SEFSL2GF.js → chunk-TOIEB3LG.js} +2 -2
  43. package/dist/chunk-VO4QI3LS.js +84 -0
  44. package/dist/{chunk-EMDQWNYR.js → chunk-WVK7AASK.js} +8 -8
  45. package/dist/{chunk-5WTJAXY2.js → chunk-Y3M323OX.js} +2 -2
  46. package/dist/{chunk-DCKMSTJ4.js → chunk-Y4JFVQ7C.js} +2 -2
  47. package/dist/{chunk-UNTPVD36.js → chunk-YAFWL3RA.js} +4 -4
  48. package/dist/{chunk-FGXRVW7G.js → chunk-YZ6L7GFO.js} +2 -2
  49. package/dist/cli.js +1401 -717
  50. package/dist/{db-BxaevAyc.d.ts → db-BHYam4BK.d.ts} +7 -19
  51. package/dist/index.d.ts +15 -15
  52. package/dist/index.js +263 -234
  53. package/dist/postinstall.js +5 -76
  54. package/dist/queries/affected.d.ts +1 -1
  55. package/dist/queries/affected.js +3 -3
  56. package/dist/queries/bottlenecks.d.ts +1 -1
  57. package/dist/queries/bottlenecks.js +2 -2
  58. package/dist/queries/by-kind.d.ts +1 -1
  59. package/dist/queries/by-kind.js +2 -2
  60. package/dist/queries/call-graph.d.ts +1 -1
  61. package/dist/queries/call-graph.js +3 -3
  62. package/dist/queries/change-surface.d.ts +2 -2
  63. package/dist/queries/change-surface.js +2 -3
  64. package/dist/queries/code.d.ts +1 -1
  65. package/dist/queries/code.js +3 -3
  66. package/dist/queries/complexity-hotspots.d.ts +1 -1
  67. package/dist/queries/complexity-hotspots.js +3 -3
  68. package/dist/queries/complexity.d.ts +1 -1
  69. package/dist/queries/complexity.js +3 -3
  70. package/dist/queries/convergence.d.ts +1 -1
  71. package/dist/queries/convergence.js +3 -3
  72. package/dist/queries/coupling.d.ts +1 -1
  73. package/dist/queries/coupling.js +1 -1
  74. package/dist/queries/cycles.d.ts +1 -1
  75. package/dist/queries/cycles.js +3 -2
  76. package/dist/queries/dataflow.d.ts +1 -1
  77. package/dist/queries/dataflow.js +3 -3
  78. package/dist/queries/dead.d.ts +1 -1
  79. package/dist/queries/dead.js +4 -3
  80. package/dist/queries/deep-chains.d.ts +1 -1
  81. package/dist/queries/deep-chains.js +3 -2
  82. package/dist/queries/deps.d.ts +1 -1
  83. package/dist/queries/diff-impact.d.ts +2 -2
  84. package/dist/queries/diff-impact.js +2 -3
  85. package/dist/queries/doc-coverage.d.ts +1 -1
  86. package/dist/queries/doc-coverage.js +2 -2
  87. package/dist/queries/drift.d.ts +1 -1
  88. package/dist/queries/drift.js +3 -2
  89. package/dist/queries/extract-candidates.d.ts +1 -1
  90. package/dist/queries/extract-candidates.js +3 -3
  91. package/dist/queries/fan.d.ts +1 -1
  92. package/dist/queries/fan.js +2 -2
  93. package/dist/queries/files.d.ts +1 -1
  94. package/dist/queries/health.d.ts +1 -1
  95. package/dist/queries/health.js +15 -15
  96. package/dist/queries/hierarchy.d.ts +1 -1
  97. package/dist/queries/hierarchy.js +3 -2
  98. package/dist/queries/hotspots.d.ts +1 -1
  99. package/dist/queries/hotspots.js +2 -2
  100. package/dist/queries/imports.d.ts +1 -1
  101. package/dist/queries/imports.js +3 -2
  102. package/dist/queries/index.d.ts +1 -2
  103. package/dist/queries/index.js +46 -51
  104. package/dist/queries/isolated.d.ts +1 -1
  105. package/dist/queries/isolated.js +4 -3
  106. package/dist/queries/members.d.ts +2 -2
  107. package/dist/queries/members.js +3 -2
  108. package/dist/queries/methods.d.ts +1 -1
  109. package/dist/queries/methods.js +2 -2
  110. package/dist/queries/outline.d.ts +1 -1
  111. package/dist/queries/outline.js +2 -2
  112. package/dist/queries/passthrough-candidates.d.ts +1 -1
  113. package/dist/queries/passthrough-candidates.js +3 -3
  114. package/dist/queries/redundant-reexports.d.ts +1 -1
  115. package/dist/queries/redundant-reexports.js +4 -2
  116. package/dist/queries/refs.d.ts +1 -1
  117. package/dist/queries/refs.js +1 -1
  118. package/dist/queries/similar-chains.d.ts +1 -1
  119. package/dist/queries/similar-chains.js +3 -2
  120. package/dist/queries/similar-files.d.ts +1 -1
  121. package/dist/queries/similar-files.js +3 -2
  122. package/dist/queries/similar-signatures.d.ts +1 -1
  123. package/dist/queries/similar-signatures.js +2 -2
  124. package/dist/queries/similar.d.ts +1 -1
  125. package/dist/queries/similar.js +3 -3
  126. package/dist/queries/slice.d.ts +1 -1
  127. package/dist/queries/slice.js +3 -3
  128. package/dist/queries/stale-abstractions.d.ts +1 -1
  129. package/dist/queries/stale-abstractions.js +3 -3
  130. package/dist/queries/stats.d.ts +1 -1
  131. package/dist/queries/stats.js +1 -1
  132. package/dist/queries/surface.d.ts +1 -1
  133. package/dist/queries/surface.js +2 -2
  134. package/dist/queries/symbols.d.ts +1 -1
  135. package/dist/queries/symbols.js +2 -2
  136. package/dist/queries/system.d.ts +1 -1
  137. package/dist/queries/system.js +2 -2
  138. package/dist/queries/trace.d.ts +1 -1
  139. package/dist/queries/trace.js +3 -1
  140. package/dist/queries/wrapper-candidates.d.ts +1 -1
  141. package/dist/queries/wrapper-candidates.js +3 -3
  142. package/dist/reindex-worker.js +24 -12
  143. package/package.json +6 -1
  144. package/IMPROVEMENTS.md +0 -143
  145. package/PLAN.md +0 -320
  146. package/dist/chunk-2QZ23IBN.js.map +0 -1
  147. package/dist/chunk-36OMT7ZJ.js.map +0 -1
  148. package/dist/chunk-3E2X7RIE.js.map +0 -1
  149. package/dist/chunk-3UOUTZQT.js +0 -45
  150. package/dist/chunk-3UOUTZQT.js.map +0 -1
  151. package/dist/chunk-3ZZJVBIO.js +0 -88
  152. package/dist/chunk-3ZZJVBIO.js.map +0 -1
  153. package/dist/chunk-4TYLS5XX.js.map +0 -1
  154. package/dist/chunk-5FGUEU7N.js.map +0 -1
  155. package/dist/chunk-5WTJAXY2.js.map +0 -1
  156. package/dist/chunk-6NBLIDF4.js.map +0 -1
  157. package/dist/chunk-6SXADWLW.js.map +0 -1
  158. package/dist/chunk-6VJ6Q7IE.js.map +0 -1
  159. package/dist/chunk-7OZPA5OO.js.map +0 -1
  160. package/dist/chunk-BEPIEVLR.js +0 -76
  161. package/dist/chunk-BEPIEVLR.js.map +0 -1
  162. package/dist/chunk-BFSCMC22.js.map +0 -1
  163. package/dist/chunk-BP2ATLK2.js.map +0 -1
  164. package/dist/chunk-CM454WL3.js.map +0 -1
  165. package/dist/chunk-DCKMSTJ4.js.map +0 -1
  166. package/dist/chunk-DEZKCZXD.js +0 -40
  167. package/dist/chunk-DEZKCZXD.js.map +0 -1
  168. package/dist/chunk-DVWGWHFW.js +0 -99
  169. package/dist/chunk-DVWGWHFW.js.map +0 -1
  170. package/dist/chunk-EMDQWNYR.js.map +0 -1
  171. package/dist/chunk-FFSWWE5O.js.map +0 -1
  172. package/dist/chunk-FGXRVW7G.js.map +0 -1
  173. package/dist/chunk-FUHJCHS4.js.map +0 -1
  174. package/dist/chunk-GJFURBEW.js.map +0 -1
  175. package/dist/chunk-GTILYBH6.js.map +0 -1
  176. package/dist/chunk-JJP7KQND.js +0 -1
  177. package/dist/chunk-JJP7KQND.js.map +0 -1
  178. package/dist/chunk-JKP5GH6T.js.map +0 -1
  179. package/dist/chunk-KCBMVQL5.js.map +0 -1
  180. package/dist/chunk-KVSW5KYP.js.map +0 -1
  181. package/dist/chunk-LAWMH22O.js.map +0 -1
  182. package/dist/chunk-LB7OS35Q.js.map +0 -1
  183. package/dist/chunk-LUSIFBXO.js.map +0 -1
  184. package/dist/chunk-MBVNHJVN.js.map +0 -1
  185. package/dist/chunk-MGNMHKX3.js.map +0 -1
  186. package/dist/chunk-N5KEREIA.js.map +0 -1
  187. package/dist/chunk-NDSQYIWT.js.map +0 -1
  188. package/dist/chunk-NUZ4OMU3.js.map +0 -1
  189. package/dist/chunk-QOV2R2WT.js.map +0 -1
  190. package/dist/chunk-SEFSL2GF.js.map +0 -1
  191. package/dist/chunk-T6ARFSBZ.js.map +0 -1
  192. package/dist/chunk-TBP6BICL.js.map +0 -1
  193. package/dist/chunk-TDNNOR6D.js.map +0 -1
  194. package/dist/chunk-TSPZOMHC.js.map +0 -1
  195. package/dist/chunk-UNTPVD36.js.map +0 -1
  196. package/dist/chunk-VRUJH4BO.js.map +0 -1
  197. package/dist/chunk-VZ7AMAFL.js.map +0 -1
  198. package/dist/chunk-XFXDXEUN.js.map +0 -1
  199. package/dist/chunk-YZAA4LYG.js.map +0 -1
  200. package/dist/chunk-Z73NYSBZ.js.map +0 -1
  201. package/dist/chunk-ZJRYBOEE.js.map +0 -1
  202. package/dist/cli.js.map +0 -1
  203. package/dist/index.js.map +0 -1
  204. package/dist/postinstall.js.map +0 -1
  205. package/dist/queries/affected.js.map +0 -1
  206. package/dist/queries/bottlenecks.js.map +0 -1
  207. package/dist/queries/by-kind.js.map +0 -1
  208. package/dist/queries/call-graph.js.map +0 -1
  209. package/dist/queries/change-surface.js.map +0 -1
  210. package/dist/queries/clean-signature.js.map +0 -1
  211. package/dist/queries/code.js.map +0 -1
  212. package/dist/queries/complexity-hotspots.js.map +0 -1
  213. package/dist/queries/complexity.js.map +0 -1
  214. package/dist/queries/convergence.js.map +0 -1
  215. package/dist/queries/coupling.js.map +0 -1
  216. package/dist/queries/cycles.js.map +0 -1
  217. package/dist/queries/dataflow.js.map +0 -1
  218. package/dist/queries/dead.js.map +0 -1
  219. package/dist/queries/deep-chains.js.map +0 -1
  220. package/dist/queries/deps.js.map +0 -1
  221. package/dist/queries/diff-impact.js.map +0 -1
  222. package/dist/queries/doc-coverage.js.map +0 -1
  223. package/dist/queries/drift.js.map +0 -1
  224. package/dist/queries/extract-candidates.js.map +0 -1
  225. package/dist/queries/fan.js.map +0 -1
  226. package/dist/queries/files.js.map +0 -1
  227. package/dist/queries/health.js.map +0 -1
  228. package/dist/queries/hierarchy.js.map +0 -1
  229. package/dist/queries/hotspots.js.map +0 -1
  230. package/dist/queries/imports.js.map +0 -1
  231. package/dist/queries/index.js.map +0 -1
  232. package/dist/queries/isolated.js.map +0 -1
  233. package/dist/queries/members.js.map +0 -1
  234. package/dist/queries/methods.js.map +0 -1
  235. package/dist/queries/outline.js.map +0 -1
  236. package/dist/queries/passthrough-candidates.js.map +0 -1
  237. package/dist/queries/redundant-reexports.js.map +0 -1
  238. package/dist/queries/refs.js.map +0 -1
  239. package/dist/queries/similar-chains.js.map +0 -1
  240. package/dist/queries/similar-files.js.map +0 -1
  241. package/dist/queries/similar-signatures.js.map +0 -1
  242. package/dist/queries/similar.js.map +0 -1
  243. package/dist/queries/slice.js.map +0 -1
  244. package/dist/queries/stale-abstractions.js.map +0 -1
  245. package/dist/queries/stats.js.map +0 -1
  246. package/dist/queries/surface.js.map +0 -1
  247. package/dist/queries/symbols.js.map +0 -1
  248. package/dist/queries/system.js.map +0 -1
  249. package/dist/queries/test-coverage.d.ts +0 -22
  250. package/dist/queries/test-coverage.js +0 -11
  251. package/dist/queries/test-coverage.js.map +0 -1
  252. package/dist/queries/trace.js.map +0 -1
  253. package/dist/queries/wrapper-candidates.js.map +0 -1
  254. package/dist/reindex-worker.js.map +0 -1
  255. package/docs/AGENT_GUIDE.md +0 -359
  256. package/reports/debloat/2026-04-10-scip-query-self-audit.md +0 -161
  257. package/src/cli.ts +0 -1480
  258. package/src/config.ts +0 -117
  259. package/src/db.ts +0 -127
  260. package/src/gitignore-filter.ts +0 -143
  261. package/src/index.ts +0 -11
  262. package/src/postinstall.ts +0 -8
  263. package/src/queries/affected.ts +0 -86
  264. package/src/queries/bottlenecks.ts +0 -67
  265. package/src/queries/by-kind.ts +0 -204
  266. package/src/queries/call-graph.ts +0 -66
  267. package/src/queries/change-surface.ts +0 -110
  268. package/src/queries/clean-signature.ts +0 -22
  269. package/src/queries/code.ts +0 -101
  270. package/src/queries/complexity-hotspots.ts +0 -119
  271. package/src/queries/complexity.ts +0 -152
  272. package/src/queries/convergence.ts +0 -82
  273. package/src/queries/coupling.ts +0 -99
  274. package/src/queries/cycles.ts +0 -78
  275. package/src/queries/dataflow.ts +0 -128
  276. package/src/queries/dead.ts +0 -122
  277. package/src/queries/deep-chains.ts +0 -59
  278. package/src/queries/deps.ts +0 -46
  279. package/src/queries/diff-impact.ts +0 -204
  280. package/src/queries/doc-coverage.ts +0 -86
  281. package/src/queries/drift.ts +0 -224
  282. package/src/queries/extract-candidates.ts +0 -167
  283. package/src/queries/fan.ts +0 -148
  284. package/src/queries/files.ts +0 -16
  285. package/src/queries/health.ts +0 -324
  286. package/src/queries/hierarchy.ts +0 -49
  287. package/src/queries/hotspots.ts +0 -53
  288. package/src/queries/imports.ts +0 -95
  289. package/src/queries/index.ts +0 -45
  290. package/src/queries/isolated.ts +0 -67
  291. package/src/queries/members.ts +0 -54
  292. package/src/queries/methods.ts +0 -27
  293. package/src/queries/outline.ts +0 -52
  294. package/src/queries/passthrough-candidates.ts +0 -94
  295. package/src/queries/redundant-reexports.ts +0 -170
  296. package/src/queries/refs.ts +0 -27
  297. package/src/queries/similar-chains.ts +0 -314
  298. package/src/queries/similar-files.ts +0 -140
  299. package/src/queries/similar-signatures.ts +0 -151
  300. package/src/queries/similar.ts +0 -305
  301. package/src/queries/slice.ts +0 -154
  302. package/src/queries/stale-abstractions.ts +0 -82
  303. package/src/queries/stats.ts +0 -22
  304. package/src/queries/surface.ts +0 -34
  305. package/src/queries/symbols.ts +0 -39
  306. package/src/queries/system.ts +0 -86
  307. package/src/queries/test-coverage.ts +0 -106
  308. package/src/queries/trace.ts +0 -55
  309. package/src/queries/wrapper-candidates.ts +0 -112
  310. package/src/query-support.ts +0 -226
  311. package/src/reindex/detect.ts +0 -58
  312. package/src/reindex/index.ts +0 -153
  313. package/src/reindex/indexers.ts +0 -220
  314. package/src/reindex/install.ts +0 -125
  315. package/src/reindex-worker.ts +0 -35
  316. package/src/setup.ts +0 -202
  317. package/src/symbol-parser.ts +0 -278
  318. package/src/types.ts +0 -654
  319. package/src/watch.ts +0 -274
  320. package/tests/gitignore-filter.test.ts +0 -48
  321. package/tests/queries.test.ts +0 -300
  322. package/tests/symbol-parser.test.ts +0 -157
  323. package/tsconfig.json +0 -20
  324. package/tsup.config.ts +0 -40
  325. package/vitest.config.ts +0 -7
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/query-support.ts"],"sourcesContent":["import type { ScipDatabase } from './db.js';\n\nexport interface SymbolLocation {\n documentId: number;\n startLine: number;\n endLine: number;\n symbolId: number;\n}\n\nexport interface SymbolMatch extends SymbolLocation {\n symbol: string;\n relativePath: string;\n}\n\nexport interface CalleeRow {\n symbol: string;\n file: string;\n chunkId: number;\n}\n\nexport const TEST_FILE_PATTERNS = [\n '%/__tests__/%',\n '%.test.%',\n '%.spec.%',\n '%/test/%',\n '%/tests/%',\n '%_test.%',\n '%_spec.%',\n '%/test_%.%',\n '%/spec_%.%',\n] as const;\n\nexport const TEST_SUPPORT_PATH_PATTERNS = [\n '%/test-utils/%',\n] as const;\n\nexport function testFileMatchSql(\n alias: string,\n patterns: readonly string[] = TEST_FILE_PATTERNS,\n): string {\n return `(${patterns.map((pattern) => `${alias}.relative_path LIKE '${pattern}'`).join(' OR ')})`;\n}\n\nexport function testFileExclusionSql(\n alias: string,\n extraPatterns: readonly string[] = [],\n): string {\n const patterns = uniquePatterns([...TEST_FILE_PATTERNS, ...extraPatterns]);\n return patterns\n .map((pattern) => `${alias}.relative_path NOT LIKE '${pattern}'`)\n .join('\\n AND ');\n}\n\nexport function buildFileDepGraph(\n db: ScipDatabase,\n scope?: string,\n): Map<string, Set<string>> {\n const scopeFilter = scope ? `AND d1.relative_path LIKE '%${scope}%'` : '';\n\n const edges = db.all<{ from_file: string; to_file: string }>(\n `SELECT DISTINCT\n d1.relative_path AS from_file,\n d2.relative_path AS to_file\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d1 ON c.document_id = d1.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d2 ON der.document_id = d2.id\n WHERE d1.id != d2.id\n AND m.role = 0\n ${db.pathExclusionsFor('d1', 'd2')}\n ${scopeFilter}`,\n );\n\n const graph = new Map<string, Set<string>>();\n for (const edge of edges) {\n if (db.isIgnored(edge.from_file) || db.isIgnored(edge.to_file)) continue;\n if (!graph.has(edge.from_file)) graph.set(edge.from_file, new Set());\n graph.get(edge.from_file)!.add(edge.to_file);\n }\n\n return graph;\n}\n\nexport function findFirstSymbolMatch(\n db: ScipDatabase,\n symbolPattern: string,\n): SymbolMatch | null {\n // Handle file:line-line syntax (e.g., \"src/foo.ts:10-50\")\n const fileLineMatch = symbolPattern.match(/^(.+):(\\d+)-(\\d+)$/);\n if (fileLineMatch) {\n const [, filePath, startStr, endStr] = fileLineMatch;\n const row = db.get<{\n id: number;\n symbol: string;\n document_id: number;\n start_line: number;\n end_line: number;\n relative_path: string;\n }>(\n `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE d.relative_path LIKE ?\n AND der.start_line <= ? AND der.end_line >= ?\n ${db.pathExclusionsFor('d')}\n ORDER BY (der.end_line - der.start_line) ASC\n LIMIT 1`,\n `%${filePath}%`, parseInt(startStr!, 10), parseInt(endStr!, 10),\n );\n if (row && !db.isIgnored(row.relative_path)) {\n return {\n symbolId: row.id,\n symbol: row.symbol,\n documentId: row.document_id,\n startLine: row.start_line,\n endLine: row.end_line,\n relativePath: row.relative_path,\n };\n }\n }\n\n // Strip parentheses from the pattern to avoid shell escaping issues.\n // Agents often pass \"functionName()\" — strip the () for matching.\n const cleaned = symbolPattern.replace(/\\(\\)$/, '').replace(/\\(.*$/, '');\n\n // Try exact-ish match first (with noise filter), then fallback without noise filter.\n // The noise filter excludes %().(% which can incorrectly match some symbols.\n for (const useNoiseFilter of [true, false]) {\n const noiseClause = useNoiseFilter ? db.symbolNoiseFor('gs') : '';\n const row = db.get<{\n id: number;\n symbol: string;\n document_id: number;\n start_line: number;\n end_line: number;\n relative_path: string;\n }>(\n `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE gs.symbol LIKE ?\n ${db.pathExclusionsFor('d')}\n ${noiseClause}\n ORDER BY (der.end_line - der.start_line) DESC\n LIMIT 1`,\n `%${cleaned}%`,\n );\n\n if (row && !db.isIgnored(row.relative_path)) {\n return {\n symbolId: row.id,\n symbol: row.symbol,\n documentId: row.document_id,\n startLine: row.start_line,\n endLine: row.end_line,\n relativePath: row.relative_path,\n };\n }\n }\n\n return null;\n}\n\nexport function getCalleeRowsForSymbol(\n db: ScipDatabase,\n symbol: SymbolLocation,\n opts: { limit?: number } = {},\n): CalleeRow[] {\n const rows = db.all<{\n symbol: string;\n file: string;\n chunk_id: number;\n }>(\n `SELECT DISTINCT\n callee_gs.symbol AS symbol,\n callee_d.relative_path AS file,\n c.id AS chunk_id\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN global_symbols callee_gs ON m.symbol_id = callee_gs.id\n JOIN defn_enclosing_ranges callee_der ON callee_gs.id = callee_der.symbol_id\n JOIN documents callee_d ON callee_der.document_id = callee_d.id\n WHERE c.document_id = ?\n AND c.start_line >= ?\n AND c.end_line <= ?\n AND m.role = 0\n AND callee_gs.id != ?\n ${db.symbolNoiseFor('callee_gs')}\n ${db.pathExclusionsFor('callee_d')}\n ORDER BY callee_d.relative_path\n ${opts.limit ? 'LIMIT ?' : ''}`,\n ...calleeQueryParams(symbol, opts.limit),\n );\n\n return rows.filter((row) => !db.isIgnored(row.file)).map((row) => ({\n symbol: row.symbol,\n file: row.file,\n chunkId: row.chunk_id,\n }));\n}\n\nfunction calleeQueryParams(\n symbol: SymbolLocation,\n limit?: number,\n): number[] {\n const params = [\n symbol.documentId,\n symbol.startLine,\n symbol.endLine,\n symbol.symbolId,\n ];\n\n if (typeof limit === 'number') {\n params.push(limit);\n }\n\n return params;\n}\n\nfunction uniquePatterns(patterns: readonly string[]): string[] {\n return [...new Set(patterns)];\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/stale-abstractions.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { testFileExclusionSql } from '../query-support.js';\nimport type { StaleAbstraction } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find stale abstractions: type-level symbols (classes, interfaces, type\n * aliases) that have 0 or 1 cross-file consumers.\n *\n * A type that only one file uses is over-abstracted — it was designed\n * for reuse that never materialized. Large single-consumer types are\n * the strongest signal of wasted abstraction.\n */\nexport function staleAbstractions(\n db: ScipDatabase,\n opts?: { scope?: string; minLoc?: number; limit?: number },\n): StaleAbstraction[] {\n const { scope, minLoc = 3, limit = 30 } = opts ?? {};\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n const rows = db.all<{\n symbol: string;\n file: string;\n start_line: number;\n end_line: number;\n loc: number;\n consumers: number;\n }>(\n `SELECT * FROM (\n SELECT\n gs.symbol,\n d.relative_path AS file,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n (SELECT COUNT(DISTINCT ref_c.document_id)\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n WHERE ref_m.symbol_id = gs.id\n AND ref_m.role = 0\n AND ref_c.document_id != der.document_id\n ) AS consumers\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testFileExclusionSql('d')}\n ${db.symbolNoiseFor('gs')}\n -- Top-level type symbols: ends with # but does not contain nested #\n AND gs.symbol LIKE '%#'\n AND gs.symbol NOT LIKE '%#%#%'\n AND (der.end_line - der.start_line + 1) >= ?\n ${scopeFilter}\n ) WHERE consumers <= 1\n ORDER BY loc DESC\n LIMIT ?`,\n minLoc, limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.file))\n // Exclude types defined in dedicated type files (types.ts, types/, etc.)\n // These are intentional public API types, not premature abstractions.\n .filter((r) => {\n const basename = r.file.split('/').pop() ?? '';\n const isTypeFile = basename.includes('types') || r.file.includes('/types/');\n // Types in type files with 1 consumer are normal API types — skip them.\n // Types in type files with 0 consumers are genuinely unused — keep them.\n if (isTypeFile && r.consumers > 0) return false;\n return true;\n })\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n file: r.file,\n startLine: r.start_line,\n endLine: r.end_line,\n loc: r.loc,\n consumers: r.consumers,\n }));\n}\n"],"mappings":";;;;;;;;AAaO,SAAS,kBACd,IACA,MACoB;AACpB,QAAM,EAAE,OAAO,SAAS,GAAG,QAAQ,GAAG,IAAI,QAAQ,CAAC;AACnD,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAEtE,QAAM,OAAO,GAAG;AAAA,IAQd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAkBM,GAAG,kBAAkB,GAAG,CAAC;AAAA,cACrB,qBAAqB,GAAG,CAAC;AAAA,UAC7B,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,UAKvB,WAAW;AAAA;AAAA;AAAA;AAAA,IAIjB;AAAA,IAAQ;AAAA,EACV;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EAGnC,OAAO,CAAC,MAAM;AACb,UAAM,WAAW,EAAE,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AAC5C,UAAM,aAAa,SAAS,SAAS,OAAO,KAAK,EAAE,KAAK,SAAS,SAAS;AAG1E,QAAI,cAAc,EAAE,YAAY,EAAG,QAAO;AAC1C,WAAO;AAAA,EACT,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,MAAM,EAAE;AAAA,IACR,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,KAAK,EAAE;AAAA,IACP,WAAW,EAAE;AAAA,EACf,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/fan.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { FanResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Fan-in: how many distinct files reference this symbol.\n * High fan-in = widely depended upon = high blast radius for changes.\n */\nexport function fanIn(\n db: ScipDatabase,\n symbolPattern: string,\n): FanResult[] {\n const rows = db.all<{\n symbol: string;\n file_count: number;\n }>(\n `SELECT gs.symbol, COUNT(DISTINCT c.document_id) AS file_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n WHERE gs.symbol LIKE ?\n AND m.role = 0\n GROUP BY gs.id\n ORDER BY file_count DESC`,\n `%${symbolPattern}%`,\n );\n\n return rows.map((r) => ({\n name: shortenSymbol(r.symbol),\n count: r.file_count,\n }));\n}\n\n/**\n * Fan-out: how many external symbols does this file reference.\n * High fan-out = depends on many things = fragile to upstream changes.\n */\nexport function fanOut(\n db: ScipDatabase,\n filePattern: string,\n): FanResult[] {\n const rows = db.all<{\n relative_path: string;\n symbol_count: number;\n }>(\n `SELECT d.relative_path, COUNT(DISTINCT gs.id) AS symbol_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE d.relative_path LIKE ?\n AND m.role = 0\n AND def_d.id != d.id\n GROUP BY d.id\n ORDER BY symbol_count DESC`,\n `%${filePattern}%`,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n name: r.relative_path,\n count: r.symbol_count,\n }));\n}\n\n/**\n * Top fan-in across the whole codebase — the most depended-on symbols.\n */\nexport function topFanIn(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string } = {},\n): FanResult[] {\n const { limit = 30, scope } = opts;\n const scopeFilter = scope\n ? `AND def_d.relative_path LIKE '%${scope}%'`\n : '';\n\n const rows = db.all<{\n symbol: string;\n file_count: number;\n }>(\n `SELECT gs.symbol, COUNT(DISTINCT c.document_id) AS file_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE m.role = 0\n ${db.pathExclusionsFor('def_d')}\n ${db.symbolNoiseFor('gs')}\n ${scopeFilter}\n GROUP BY gs.id\n HAVING file_count > 1\n ORDER BY file_count DESC\n LIMIT ?`,\n limit,\n );\n\n return rows.map((r) => ({\n name: shortenSymbol(r.symbol),\n count: r.file_count,\n }));\n}\n\n/**\n * Top fan-out across the whole codebase — files that depend on the most external symbols.\n */\nexport function topFanOut(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string } = {},\n): FanResult[] {\n const { limit = 30, scope } = opts;\n const scopeFilter = scope\n ? `AND d.relative_path LIKE '%${scope}%'`\n : '';\n\n const rows = db.all<{\n relative_path: string;\n symbol_count: number;\n }>(\n `SELECT d.relative_path, COUNT(DISTINCT gs.id) AS symbol_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE m.role = 0\n AND def_d.id != d.id\n ${db.pathExclusionsFor('d')}\n ${db.symbolNoiseFor('gs')}\n ${scopeFilter}\n GROUP BY d.id\n ORDER BY symbol_count DESC\n LIMIT ?`,\n limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n name: r.relative_path,\n count: r.symbol_count,\n }));\n}\n"],"mappings":";;;;;AAQO,SAAS,MACd,IACA,eACa;AACb,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,IAAI,aAAa;AAAA,EACnB;AAEA,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,MAAM,cAAc,EAAE,MAAM;AAAA,IAC5B,OAAO,EAAE;AAAA,EACX,EAAE;AACJ;AAMO,SAAS,OACd,IACA,aACa;AACb,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYA,IAAI,WAAW;AAAA,EACjB;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,EACX,EAAE;AACN;AAKO,SAAS,SACd,IACA,OAA2C,CAAC,GAC/B;AACb,QAAM,EAAE,QAAQ,IAAI,MAAM,IAAI;AAC9B,QAAM,cAAc,QAChB,kCAAkC,KAAK,OACvC;AAEJ,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOI,GAAG,kBAAkB,OAAO,CAAC;AAAA,QAC7B,GAAG,eAAe,IAAI,CAAC;AAAA,QACvB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,IAKf;AAAA,EACF;AAEA,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,MAAM,cAAc,EAAE,MAAM;AAAA,IAC5B,OAAO,EAAE;AAAA,EACX,EAAE;AACJ;AAKO,SAAS,UACd,IACA,OAA2C,CAAC,GAC/B;AACb,QAAM,EAAE,QAAQ,IAAI,MAAM,IAAI;AAC9B,QAAM,cAAc,QAChB,8BAA8B,KAAK,OACnC;AAEJ,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QASI,GAAG,kBAAkB,GAAG,CAAC;AAAA,QACzB,GAAG,eAAe,IAAI,CAAC;AAAA,QACvB,WAAW;AAAA;AAAA;AAAA;AAAA,IAIf;AAAA,EACF;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,EACX,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- //# sourceMappingURL=chunk-JJP7KQND.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/similar-chains.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { buildFileDepGraph } from '../query-support.js';\nimport type { SimilarChainResult } from '../types.js';\n\n/**\n * Find end-to-end dependency flows that are structurally similar\n * but diverge at a few points — indicating duplicated pipelines\n * that could be consolidated.\n *\n * Uses infrastructure-filtered chain comparison:\n *\n * 1. Build all dependency chains via DFS\n * 2. Compute node frequency across all chains\n * 3. Filter out infrastructure nodes (appearing in >50% of chains)\n * 4. Compare the filtered chains — what's left is the unique pipeline\n * 5. Edit distance on filtered chains finds real structural duplicates\n *\n * Two chains that both pass through db.ts → types.ts is meaningless.\n * Two chains that both pass through userValidation → userRepo → emailService\n * after filtering is a strong consolidation signal.\n */\nexport function similarChains(\n db: ScipDatabase,\n opts: {\n minSimilarity?: number;\n limit?: number;\n scope?: string;\n minChainLength?: number;\n maxChainLength?: number;\n } = {},\n): SimilarChainResult[] {\n const {\n minSimilarity = 0.5,\n limit = 15,\n scope,\n minChainLength = 3,\n maxChainLength = 8,\n } = opts;\n\n const graph = buildFileDepGraph(db, scope);\n const rawChains = generateChains(graph, minChainLength, maxChainLength);\n\n if (rawChains.length === 0) return [];\n\n // Compute node frequency to identify infrastructure.\n // Count both general frequency (appears anywhere in chain) and\n // tail frequency (appears as one of the last 2 nodes). Shared\n // tails like \"→ db.ts → types.ts\" are infrastructure even if\n // they don't appear in >50% of chains overall.\n const nodeFreq = new Map<string, number>();\n const tailFreq = new Map<string, number>();\n for (const chain of rawChains) {\n const seen = new Set<string>();\n for (const node of chain) {\n if (!seen.has(node)) {\n nodeFreq.set(node, (nodeFreq.get(node) ?? 0) + 1);\n seen.add(node);\n }\n }\n // Track tail nodes (last 2)\n for (let t = Math.max(0, chain.length - 2); t < chain.length; t++) {\n tailFreq.set(chain[t]!, (tailFreq.get(chain[t]!) ?? 0) + 1);\n }\n }\n\n // Infrastructure: nodes in >40% of chains OR tail nodes in >30% of chains\n const infraThreshold = rawChains.length * 0.4;\n const tailThreshold = rawChains.length * 0.3;\n const infraNodes = new Set<string>();\n for (const [node, freq] of nodeFreq) {\n if (freq > infraThreshold) infraNodes.add(node);\n }\n for (const [node, freq] of tailFreq) {\n if (freq > tailThreshold) infraNodes.add(node);\n }\n\n // Also treat structural role files as infrastructure — entry points,\n // barrels, and orchestrators are not meaningful pipeline nodes.\n const structuralNames = ['index.ts', 'index.js', 'cli.ts', 'main.ts', 'health.ts', 'health.js'];\n for (const node of nodeFreq.keys()) {\n const basename = node.split('/').pop() ?? '';\n if (structuralNames.includes(basename)) infraNodes.add(node);\n }\n\n // Filter chains: remove infrastructure nodes, keep the \"unique pipeline\"\n const filteredChains: { original: string[]; filtered: string[] }[] = [];\n for (const chain of rawChains) {\n const filtered = chain.filter((n) => !infraNodes.has(n));\n // Only keep chains that have at least 3 non-infrastructure nodes.\n // Chains with 1-2 unique nodes are just \"query → infra\" which is\n // the expected pattern, not a consolidation opportunity.\n if (filtered.length >= 3) {\n filteredChains.push({ original: chain, filtered });\n }\n }\n\n if (filteredChains.length < 2) return [];\n\n // Pairwise comparison on filtered chains\n const results: SimilarChainResult[] = [];\n\n for (let i = 0; i < filteredChains.length; i++) {\n for (let j = i + 1; j < filteredChains.length; j++) {\n const a = filteredChains[i]!;\n const b = filteredChains[j]!;\n\n // Quick reject: filtered chains must share at least one non-infra node\n const setA = new Set(a.filtered);\n let hasShared = false;\n for (const node of b.filtered) {\n if (setA.has(node)) { hasShared = true; break; }\n }\n if (!hasShared) continue;\n\n // Edit distance on the FILTERED chains (infrastructure stripped)\n const { distance, ops } = editDistance(a.filtered, b.filtered);\n const maxLen = Math.max(a.filtered.length, b.filtered.length);\n if (maxLen === 0) continue;\n\n const similarity = 1 - distance / maxLen;\n if (similarity < minSimilarity) continue;\n if (distance === 0) continue; // identical filtered chains = not interesting\n\n // Divergence points from the filtered comparison\n const divergencePoints = ops\n .filter((op) => op.type === 'substitute')\n .map((op) => ({\n index: op.indexA,\n nodeA: a.filtered[op.indexA]!,\n nodeB: b.filtered[op.indexB]!,\n }));\n\n if (divergencePoints.length === 0) continue;\n\n // Require at least 2 matching (non-divergent) filtered nodes.\n // A chain pair with 1 match and 1 divergence is just \"two things\n // that share one dependency\" — not a duplicated pipeline.\n const matchCount = ops.filter((op) => op.type === 'match').length;\n if (matchCount < 2) continue;\n\n // Report using original chains for context, but similarity is from filtered\n const commonPrefix = getCommonPrefix(a.original, b.original);\n const commonSuffix = getCommonSuffix(a.original, b.original);\n\n results.push({\n chainA: a.original,\n chainB: b.original,\n similarity,\n editDistance: distance,\n divergencePoints,\n commonPrefix,\n commonSuffix,\n });\n }\n\n if (results.length > limit * 10) break;\n }\n\n // Sort by similarity desc, then fewest divergence points\n results.sort((a, b) => {\n if (Math.abs(b.similarity - a.similarity) > 0.01) return b.similarity - a.similarity;\n return a.divergencePoints.length - b.divergencePoints.length;\n });\n\n // Deduplicate sub-chains\n const deduped: SimilarChainResult[] = [];\n for (const r of results) {\n const isDuplicate = deduped.some(\n (existing) =>\n isSubChain(r.chainA, existing.chainA) && isSubChain(r.chainB, existing.chainB),\n );\n if (!isDuplicate) deduped.push(r);\n if (deduped.length >= limit) break;\n }\n\n return deduped;\n}\n\n// ── Chain generation ───────────────────────────────────────\n\nfunction generateChains(\n graph: Map<string, Set<string>>,\n minLen: number,\n maxLen: number,\n): string[][] {\n const chains: string[][] = [];\n const maxChains = 500;\n\n for (const startNode of graph.keys()) {\n if (chains.length >= maxChains) break;\n dfsChains(graph, startNode, [startNode], new Set([startNode]), minLen, maxLen, chains, maxChains);\n }\n\n return chains;\n}\n\nfunction dfsChains(\n graph: Map<string, Set<string>>,\n node: string,\n path: string[],\n visited: Set<string>,\n minLen: number,\n maxLen: number,\n results: string[][],\n maxResults: number,\n): void {\n if (results.length >= maxResults) return;\n if (path.length >= maxLen) {\n if (path.length >= minLen) results.push([...path]);\n return;\n }\n\n const neighbors = graph.get(node);\n if (!neighbors || neighbors.size === 0) {\n if (path.length >= minLen) results.push([...path]);\n return;\n }\n\n let extended = false;\n for (const next of neighbors) {\n if (visited.has(next)) continue;\n visited.add(next);\n path.push(next);\n dfsChains(graph, next, path, visited, minLen, maxLen, results, maxResults);\n path.pop();\n visited.delete(next);\n extended = true;\n if (results.length >= maxResults) return;\n }\n\n if (!extended && path.length >= minLen) {\n results.push([...path]);\n }\n}\n\n// ── Edit distance ──────────────────────────────────────────\n\ninterface EditOp {\n type: 'match' | 'substitute' | 'insert' | 'delete';\n indexA: number;\n indexB: number;\n}\n\nfunction editDistance(a: string[], b: string[]): { distance: number; ops: EditOp[] } {\n const m = a.length;\n const n = b.length;\n\n const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));\n for (let i = 0; i <= m; i++) dp[i]![0] = i;\n for (let j = 0; j <= n; j++) dp[0]![j] = j;\n\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n if (a[i - 1] === b[j - 1]) {\n dp[i]![j] = dp[i - 1]![j - 1]!;\n } else {\n dp[i]![j] = 1 + Math.min(\n dp[i - 1]![j]!,\n dp[i]![j - 1]!,\n dp[i - 1]![j - 1]!,\n );\n }\n }\n }\n\n const ops: EditOp[] = [];\n let i = m, j = n;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {\n ops.unshift({ type: 'match', indexA: i - 1, indexB: j - 1 });\n i--; j--;\n } else if (i > 0 && j > 0 && dp[i]![j] === dp[i - 1]![j - 1]! + 1) {\n ops.unshift({ type: 'substitute', indexA: i - 1, indexB: j - 1 });\n i--; j--;\n } else if (j > 0 && dp[i]![j] === dp[i]![j - 1]! + 1) {\n ops.unshift({ type: 'insert', indexA: i, indexB: j - 1 });\n j--;\n } else {\n ops.unshift({ type: 'delete', indexA: i - 1, indexB: j });\n i--;\n }\n }\n\n return { distance: dp[m]![n]!, ops };\n}\n\n// ── Utility ────────────────────────────────────────────────\n\nfunction getCommonPrefix(a: string[], b: string[]): string[] {\n const prefix: string[] = [];\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n if (a[i] === b[i]) prefix.push(a[i]!);\n else break;\n }\n return prefix;\n}\n\nfunction getCommonSuffix(a: string[], b: string[]): string[] {\n const suffix: string[] = [];\n let ai = a.length - 1;\n let bi = b.length - 1;\n while (ai >= 0 && bi >= 0 && a[ai] === b[bi]) {\n suffix.unshift(a[ai]!);\n ai--; bi--;\n }\n return suffix;\n}\n\nfunction isSubChain(sub: string[], full: string[]): boolean {\n if (sub.length > full.length) return false;\n const fullStr = full.join('→');\n const subStr = sub.join('→');\n return fullStr.includes(subStr);\n}\n"],"mappings":"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","names":["i","j"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/symbols.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { SymbolResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\nimport { cleanSignature } from './clean-signature.js';\n\nexport function symbols(db: ScipDatabase, filePattern: string): SymbolResult[] {\n const rows = db.all<{\n start_line: number;\n end_line: number;\n sig: string | null;\n symbol: string;\n relative_path: string;\n }>(\n `SELECT\n der.start_line,\n der.end_line,\n REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig,\n gs.symbol,\n d.relative_path\n FROM defn_enclosing_ranges der\n JOIN global_symbols gs ON der.symbol_id = gs.id\n JOIN documents d ON der.document_id = d.id\n WHERE d.relative_path LIKE ?\n AND ${db.localSymbolPredicate}\n ${db.symbolNoise}\n ORDER BY der.start_line`,\n `%${filePattern}%`,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n startLine: r.start_line,\n endLine: r.end_line,\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n signature: cleanSignature(r.sig),\n }));\n}\n"],"mappings":";;;;;;;;AAKO,SAAS,QAAQ,IAAkB,aAAqC;AAC7E,QAAM,OAAO,GAAG;AAAA,IAOd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAUQ,GAAG,oBAAoB;AAAA,QAC3B,GAAG,WAAW;AAAA;AAAA,IAElB,IAAI,WAAW;AAAA,EACjB;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,WAAW,eAAe,EAAE,GAAG;AAAA,EACjC,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/coupling.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { CouplingResult } from '../types.js';\n\n/**\n * Measure coupling between two files: how many symbols do they share\n * (symbols defined in one and referenced in the other, or vice versa).\n */\nexport function coupling(\n db: ScipDatabase,\n file1: string,\n file2: string,\n): CouplingResult {\n const row = db.get<{ shared: number }>(\n `SELECT COUNT(DISTINCT gs.id) AS shared\n FROM global_symbols gs\n WHERE (\n -- Defined in file1, referenced in file2\n EXISTS (\n SELECT 1 FROM defn_enclosing_ranges der\n JOIN documents d ON der.document_id = d.id\n WHERE der.symbol_id = gs.id AND d.relative_path LIKE ?\n )\n AND EXISTS (\n SELECT 1 FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = gs.id AND m.role = 0 AND d.relative_path LIKE ?\n )\n ) OR (\n -- Defined in file2, referenced in file1\n EXISTS (\n SELECT 1 FROM defn_enclosing_ranges der\n JOIN documents d ON der.document_id = d.id\n WHERE der.symbol_id = gs.id AND d.relative_path LIKE ?\n )\n AND EXISTS (\n SELECT 1 FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = gs.id AND m.role = 0 AND d.relative_path LIKE ?\n )\n )`,\n `%${file1}%`, `%${file2}%`,\n `%${file2}%`, `%${file1}%`,\n );\n\n return {\n file1,\n file2,\n sharedSymbols: row?.shared ?? 0,\n };\n}\n\n/**\n * Find the most coupled file pairs in the codebase.\n */\nexport function topCoupling(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string } = {},\n): CouplingResult[] {\n const { limit = 20, scope } = opts;\n const scopeFilter = scope\n ? `AND d1.relative_path LIKE '%${scope}%' AND d2.relative_path LIKE '%${scope}%'`\n : '';\n\n // Find file pairs that share the most symbols (one defines, other references)\n const rows = db.all<{\n file1: string;\n file2: string;\n shared: number;\n }>(\n `SELECT\n def_d.relative_path AS file1,\n ref_d.relative_path AS file2,\n COUNT(DISTINCT gs.id) AS shared\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE m.role = 0\n AND def_d.id != ref_d.id\n ${db.pathExclusionsFor('def_d', 'ref_d')}\n ${scopeFilter}\n GROUP BY def_d.id, ref_d.id\n ORDER BY shared DESC\n LIMIT ?`,\n limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.file1) && !db.isIgnored(r.file2))\n .map((r) => ({\n file1: r.file1,\n file2: r.file2,\n sharedSymbols: r.shared,\n }));\n}\n"],"mappings":";AAOO,SAAS,SACd,IACA,OACA,OACgB;AAChB,QAAM,MAAM,GAAG;AAAA,IACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IA6BA,IAAI,KAAK;AAAA,IAAK,IAAI,KAAK;AAAA,IACvB,IAAI,KAAK;AAAA,IAAK,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,eAAe,KAAK,UAAU;AAAA,EAChC;AACF;AAKO,SAAS,YACd,IACA,OAA2C,CAAC,GAC1B;AAClB,QAAM,EAAE,QAAQ,IAAI,MAAM,IAAI;AAC9B,QAAM,cAAc,QAChB,+BAA+B,KAAK,kCAAkC,KAAK,OAC3E;AAGJ,QAAM,OAAO,GAAG;AAAA,IAKd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAYI,GAAG,kBAAkB,SAAS,OAAO,CAAC;AAAA,QACtC,WAAW;AAAA;AAAA;AAAA;AAAA,IAIf;AAAA,EACF;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,KAAK,KAAK,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,EAC9D,IAAI,CAAC,OAAO;AAAA,IACX,OAAO,EAAE;AAAA,IACT,OAAO,EAAE;AAAA,IACT,eAAe,EAAE;AAAA,EACnB,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/by-kind.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { ByKindResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * SCIP SymbolInformation.Kind enum values.\n * From: https://github.com/sourcegraph/scip/blob/main/scip.proto\n */\nconst KIND_NAMES: Record<number, string> = {\n 0: 'UnspecifiedKind',\n 1: 'AbstractMethod',\n 2: 'Accessor',\n 3: 'Array',\n 4: 'Assertion',\n 5: 'AssociatedType',\n 6: 'Attribute',\n 7: 'Axiom',\n 8: 'Boolean',\n 9: 'Class',\n 10: 'Constant',\n 11: 'Constructor',\n 12: 'Contract',\n 13: 'DataFamily',\n 14: 'DefinitionMacro',\n 15: 'Delegate',\n 16: 'Enum',\n 17: 'EnumMember',\n 18: 'Error',\n 19: 'Event',\n 20: 'Fact',\n 21: 'Field',\n 22: 'File',\n 23: 'Function',\n 24: 'Getter',\n 25: 'Grammar',\n 26: 'Instance',\n 27: 'Interface',\n 28: 'Key',\n 29: 'Lang',\n 30: 'Lemma',\n 31: 'Library',\n 32: 'Macro',\n 33: 'Method',\n 34: 'MethodAlias',\n 35: 'MethodReceiver',\n 36: 'MethodSpecification',\n 37: 'Message',\n 38: 'Modifier',\n 39: 'Module',\n 40: 'Namespace',\n 41: 'Null',\n 42: 'Number',\n 43: 'Object',\n 44: 'Operator',\n 45: 'Package',\n 46: 'PackageObject',\n 47: 'Parameter',\n 48: 'ParameterLabel',\n 49: 'Pattern',\n 50: 'Predicate',\n 51: 'Property',\n 52: 'Protocol',\n 53: 'ProtocolMethod',\n 54: 'PureVirtualMethod',\n 55: 'Quasiquoter',\n 56: 'SelfParameter',\n 57: 'Setter',\n 58: 'Signature',\n 59: 'SingletonClass',\n 60: 'SingletonMethod',\n 61: 'StaticDataMember',\n 62: 'StaticEvent',\n 63: 'StaticField',\n 64: 'StaticMethod',\n 65: 'StaticProperty',\n 66: 'StaticVariable',\n 67: 'String',\n 68: 'Struct',\n 69: 'Subscript',\n 70: 'Tactic',\n 71: 'Theorem',\n 72: 'ThisParameter',\n 73: 'Trait',\n 74: 'TraitMethod',\n 75: 'Type',\n 76: 'TypeAlias',\n 77: 'TypeClass',\n 78: 'TypeClassMethod',\n 79: 'TypeFamily',\n 80: 'TypeParameter',\n 81: 'Union',\n 82: 'Value',\n 83: 'Variable',\n};\n\n/** Reverse lookup: name -> kind number */\nconst KIND_BY_NAME = new Map<string, number>();\nfor (const [k, v] of Object.entries(KIND_NAMES)) {\n KIND_BY_NAME.set(v.toLowerCase(), Number(k));\n}\n\n/**\n * Find symbols by SCIP kind (class, interface, enum, function, etc.)\n */\nexport function byKind(\n db: ScipDatabase,\n kindQuery: string,\n opts: { scope?: string; limit?: number } = {},\n): ByKindResult[] {\n const { scope, limit = 100 } = opts;\n\n // Resolve kind: accept number or name\n let kindNum: number | null = null;\n const asNum = parseInt(kindQuery, 10);\n if (!isNaN(asNum)) {\n kindNum = asNum;\n } else {\n kindNum = KIND_BY_NAME.get(kindQuery.toLowerCase()) ?? null;\n // Fuzzy match: try partial name\n if (kindNum === null) {\n for (const [name, num] of KIND_BY_NAME) {\n if (name.includes(kindQuery.toLowerCase())) {\n kindNum = num;\n break;\n }\n }\n }\n }\n\n if (kindNum === null) {\n return [];\n }\n\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n // Check if the index actually has kind data populated\n const hasKinds = db.get<{ c: number }>(\n `SELECT COUNT(*) AS c FROM global_symbols WHERE kind IS NOT NULL`,\n );\n if (!hasKinds || hasKinds.c === 0) {\n return []; // Indexer doesn't populate kind field\n }\n\n const rows = db.all<{\n symbol: string;\n kind: number;\n relative_path: string;\n start_line: number;\n end_line: number;\n }>(\n `SELECT gs.symbol, gs.kind, d.relative_path, der.start_line, der.end_line\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE gs.kind = ?\n ${db.pathExclusionsFor('d')}\n ${scopeFilter}\n ORDER BY d.relative_path, der.start_line\n LIMIT ?`,\n kindNum, limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n kind: r.kind,\n kindName: KIND_NAMES[r.kind] ?? 'Unknown',\n relativePath: r.relative_path,\n startLine: r.start_line,\n endLine: r.end_line,\n }));\n}\n\n/** List all symbol kinds present in the index with counts */\nexport function kindCounts(\n db: ScipDatabase,\n opts: { scope?: string } = {},\n): Array<{ kind: number; kindName: string; count: number }> {\n const scopeFilter = opts.scope\n ? `AND d.relative_path LIKE '%${opts.scope}%'`\n : '';\n\n const rows = db.all<{ kind: number; cnt: number }>(\n `SELECT gs.kind, COUNT(*) AS cnt\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND gs.kind IS NOT NULL\n AND gs.kind != 0\n ${scopeFilter}\n GROUP BY gs.kind\n ORDER BY cnt DESC`,\n );\n\n return rows.map((r) => ({\n kind: r.kind,\n kindName: KIND_NAMES[r.kind] ?? 'Unknown',\n count: r.cnt,\n }));\n}\n"],"mappings":";;;;;AAQA,IAAM,aAAqC;AAAA,EACzC,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGA,IAAM,eAAe,oBAAI,IAAoB;AAC7C,WAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC/C,eAAa,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC;AAC7C;AAKO,SAAS,OACd,IACA,WACA,OAA2C,CAAC,GAC5B;AAChB,QAAM,EAAE,OAAO,QAAQ,IAAI,IAAI;AAG/B,MAAI,UAAyB;AAC7B,QAAM,QAAQ,SAAS,WAAW,EAAE;AACpC,MAAI,CAAC,MAAM,KAAK,GAAG;AACjB,cAAU;AAAA,EACZ,OAAO;AACL,cAAU,aAAa,IAAI,UAAU,YAAY,CAAC,KAAK;AAEvD,QAAI,YAAY,MAAM;AACpB,iBAAW,CAAC,MAAM,GAAG,KAAK,cAAc;AACtC,YAAI,KAAK,SAAS,UAAU,YAAY,CAAC,GAAG;AAC1C,oBAAU;AACV;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,YAAY,MAAM;AACpB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAGtE,QAAM,WAAW,GAAG;AAAA,IAClB;AAAA,EACF;AACA,MAAI,CAAC,YAAY,SAAS,MAAM,GAAG;AACjC,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,OAAO,GAAG;AAAA,IAOd;AAAA;AAAA;AAAA;AAAA;AAAA,QAKI,GAAG,kBAAkB,GAAG,CAAC;AAAA,QACzB,WAAW;AAAA;AAAA;AAAA,IAGf;AAAA,IAAS;AAAA,EACX;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,MAAM,EAAE;AAAA,IACR,UAAU,WAAW,EAAE,IAAI,KAAK;AAAA,IAChC,cAAc,EAAE;AAAA,IAChB,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,EACb,EAAE;AACN;AAGO,SAAS,WACd,IACA,OAA2B,CAAC,GAC8B;AAC1D,QAAM,cAAc,KAAK,QACrB,8BAA8B,KAAK,KAAK,OACxC;AAEJ,QAAM,OAAO,GAAG;AAAA,IACd;AAAA;AAAA;AAAA;AAAA;AAAA,QAKI,GAAG,kBAAkB,GAAG,CAAC;AAAA;AAAA;AAAA,QAGzB,WAAW;AAAA;AAAA;AAAA,EAGjB;AAEA,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,MAAM,EAAE;AAAA,IACR,UAAU,WAAW,EAAE,IAAI,KAAK;AAAA,IAChC,OAAO,EAAE;AAAA,EACX,EAAE;AACJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/convergence.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch, getCalleeRowsForSymbol } from '../query-support.js';\nimport type { ConvergenceResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Given two similar symbols, show what a consolidated version would look like.\n * The shared callee set becomes the common body. The unique callees become\n * the parameterization points.\n */\nexport function convergence(\n db: ScipDatabase,\n symbolPatternA: string,\n symbolPatternB: string,\n): ConvergenceResult | null {\n const matchA = findFirstSymbolMatch(db, symbolPatternA);\n const matchB = findFirstSymbolMatch(db, symbolPatternB);\n\n if (!matchA || !matchB) return null;\n\n const calleesA = new Set(\n getCalleeRowsForSymbol(db, matchA).map((r) => r.symbol),\n );\n const calleesB = new Set(\n getCalleeRowsForSymbol(db, matchB).map((r) => r.symbol),\n );\n\n const shared: string[] = [];\n for (const c of calleesA) {\n if (calleesB.has(c)) shared.push(c);\n }\n\n const uniqueA: string[] = [];\n for (const c of calleesA) {\n if (!calleesB.has(c)) uniqueA.push(c);\n }\n\n const uniqueB: string[] = [];\n for (const c of calleesB) {\n if (!calleesA.has(c)) uniqueB.push(c);\n }\n\n const union = new Set([...calleesA, ...calleesB]);\n const similarity = union.size > 0 ? shared.length / union.size : 0;\n\n // Generate a consolidation strategy description\n let strategy: string;\n if (uniqueA.length === 0 && uniqueB.length === 0) {\n strategy = 'These functions have identical callee sets. One can replace the other directly.';\n } else if (uniqueA.length === 0) {\n strategy = `A is a subset of B. A can be replaced by calling B (B does everything A does plus more).`;\n } else if (uniqueB.length === 0) {\n strategy = `B is a subset of A. B can be replaced by calling A (A does everything B does plus more).`;\n } else if (uniqueA.length <= 2 && uniqueB.length <= 2) {\n strategy = `Create a shared function with the ${shared.length} common callees. Pass the ${uniqueA.length + uniqueB.length} divergent callees as parameters or strategy callbacks.`;\n } else {\n strategy = `Extract the ${shared.length} shared callees into a common helper. Each function calls the helper plus its own unique logic (${uniqueA.length} callees in A, ${uniqueB.length} in B).`;\n }\n\n const locA = matchA.endLine - matchA.startLine + 1;\n const locB = matchB.endLine - matchB.startLine + 1;\n\n return {\n symbolA: {\n symbol: matchA.symbol,\n shortName: shortenSymbol(matchA.symbol),\n file: matchA.relativePath,\n loc: locA,\n },\n symbolB: {\n symbol: matchB.symbol,\n shortName: shortenSymbol(matchB.symbol),\n file: matchB.relativePath,\n loc: locB,\n },\n similarity,\n sharedCallees: shared.map(shortenSymbol),\n uniqueToA: uniqueA.map(shortenSymbol),\n uniqueToB: uniqueB.map(shortenSymbol),\n consolidationStrategy: strategy,\n };\n}\n"],"mappings":";;;;;;;;;AAUO,SAAS,YACd,IACA,gBACA,gBAC0B;AAC1B,QAAM,SAAS,qBAAqB,IAAI,cAAc;AACtD,QAAM,SAAS,qBAAqB,IAAI,cAAc;AAEtD,MAAI,CAAC,UAAU,CAAC,OAAQ,QAAO;AAE/B,QAAM,WAAW,IAAI;AAAA,IACnB,uBAAuB,IAAI,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM;AAAA,EACxD;AACA,QAAM,WAAW,IAAI;AAAA,IACnB,uBAAuB,IAAI,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM;AAAA,EACxD;AAEA,QAAM,SAAmB,CAAC;AAC1B,aAAW,KAAK,UAAU;AACxB,QAAI,SAAS,IAAI,CAAC,EAAG,QAAO,KAAK,CAAC;AAAA,EACpC;AAEA,QAAM,UAAoB,CAAC;AAC3B,aAAW,KAAK,UAAU;AACxB,QAAI,CAAC,SAAS,IAAI,CAAC,EAAG,SAAQ,KAAK,CAAC;AAAA,EACtC;AAEA,QAAM,UAAoB,CAAC;AAC3B,aAAW,KAAK,UAAU;AACxB,QAAI,CAAC,SAAS,IAAI,CAAC,EAAG,SAAQ,KAAK,CAAC;AAAA,EACtC;AAEA,QAAM,QAAQ,oBAAI,IAAI,CAAC,GAAG,UAAU,GAAG,QAAQ,CAAC;AAChD,QAAM,aAAa,MAAM,OAAO,IAAI,OAAO,SAAS,MAAM,OAAO;AAGjE,MAAI;AACJ,MAAI,QAAQ,WAAW,KAAK,QAAQ,WAAW,GAAG;AAChD,eAAW;AAAA,EACb,WAAW,QAAQ,WAAW,GAAG;AAC/B,eAAW;AAAA,EACb,WAAW,QAAQ,WAAW,GAAG;AAC/B,eAAW;AAAA,EACb,WAAW,QAAQ,UAAU,KAAK,QAAQ,UAAU,GAAG;AACrD,eAAW,qCAAqC,OAAO,MAAM,6BAA6B,QAAQ,SAAS,QAAQ,MAAM;AAAA,EAC3H,OAAO;AACL,eAAW,eAAe,OAAO,MAAM,mGAAmG,QAAQ,MAAM,kBAAkB,QAAQ,MAAM;AAAA,EAC1L;AAEA,QAAM,OAAO,OAAO,UAAU,OAAO,YAAY;AACjD,QAAM,OAAO,OAAO,UAAU,OAAO,YAAY;AAEjD,SAAO;AAAA,IACL,SAAS;AAAA,MACP,QAAQ,OAAO;AAAA,MACf,WAAW,cAAc,OAAO,MAAM;AAAA,MACtC,MAAM,OAAO;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,SAAS;AAAA,MACP,QAAQ,OAAO;AAAA,MACf,WAAW,cAAc,OAAO,MAAM;AAAA,MACtC,MAAM,OAAO;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA;AAAA,IACA,eAAe,OAAO,IAAI,aAAa;AAAA,IACvC,WAAW,QAAQ,IAAI,aAAa;AAAA,IACpC,WAAW,QAAQ,IAAI,aAAa;AAAA,IACpC,uBAAuB;AAAA,EACzB;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/isolated.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { testFileExclusionSql } from '../query-support.js';\nimport type { IsolatedResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find isolated symbols: defined locally, referenced by nothing,\n * and referencing nothing external. These are truly orphaned code —\n * not just unused exports, but completely disconnected from the graph.\n */\nexport function isolated(\n db: ScipDatabase,\n opts: { scope?: string; minLoc?: number } = {},\n): IsolatedResult[] {\n const { scope, minLoc = 3 } = opts;\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n const rows = db.all<{\n symbol: string;\n relative_path: string;\n start_line: number;\n end_line: number;\n loc: number;\n }>(\n `SELECT\n gs.symbol,\n d.relative_path,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testFileExclusionSql('d')}\n ${db.symbolNoiseFor('gs')}\n AND gs.symbol NOT LIKE '%#%'\n AND (der.end_line - der.start_line + 1) >= ?\n ${scopeFilter}\n -- No cross-file references TO this symbol\n AND NOT EXISTS (\n SELECT 1 FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n WHERE m.symbol_id = gs.id AND m.role = 0 AND c.document_id != d.id\n )\n -- No same-file references either\n AND NOT EXISTS (\n SELECT 1 FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n WHERE m.symbol_id = gs.id AND m.role = 0 AND c.document_id = d.id\n )\n ORDER BY loc DESC, d.relative_path`,\n minLoc,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n relativePath: r.relative_path,\n startLine: r.start_line,\n endLine: r.end_line,\n loc: r.loc,\n }));\n}\n"],"mappings":";;;;;;;;AAUO,SAAS,SACd,IACA,OAA4C,CAAC,GAC3B;AAClB,QAAM,EAAE,OAAO,SAAS,EAAE,IAAI;AAC9B,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAEtE,QAAM,OAAO,GAAG;AAAA,IAOd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUI,GAAG,kBAAkB,GAAG,CAAC;AAAA,YACrB,qBAAqB,GAAG,CAAC;AAAA,QAC7B,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA,QAGvB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAcf;AAAA,EACF;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,cAAc,EAAE;AAAA,IAChB,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,KAAK,EAAE;AAAA,EACT,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/deep-chains.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { buildFileDepGraph } from '../query-support.js';\nimport type { DeepChainResult } from '../types.js';\n\n/**\n * Find the longest transitive dependency chains between files.\n * A chain A → B → C → D means A depends on B, B on C, C on D.\n *\n * Long chains = high coupling depth = changes at the end ripple through many layers.\n */\nexport function deepChains(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string; minDepth?: number } = {},\n): DeepChainResult[] {\n const { limit = 10, scope, minDepth = 3 } = opts;\n const graph = buildFileDepGraph(db, scope);\n\n // DFS to find longest paths (with cycle detection)\n const results: DeepChainResult[] = [];\n\n function dfs(node: string, path: string[], visited: Set<string>): void {\n const neighbors = graph.get(node);\n if (!neighbors || neighbors.size === 0) {\n if (path.length >= minDepth) {\n results.push({ chain: [...path], depth: path.length });\n }\n return;\n }\n\n let extended = false;\n for (const next of neighbors) {\n if (visited.has(next)) continue; // skip cycles\n visited.add(next);\n path.push(next);\n dfs(next, path, visited);\n path.pop();\n visited.delete(next);\n extended = true;\n }\n\n // If no unvisited neighbors, this is a leaf in this path\n if (!extended && path.length >= minDepth) {\n results.push({ chain: [...path], depth: path.length });\n }\n }\n\n // Start DFS from each node\n for (const startNode of graph.keys()) {\n const visited = new Set<string>([startNode]);\n dfs(startNode, [startNode], visited);\n\n // Early termination if we have enough results\n if (results.length > limit * 10) break;\n }\n\n // Sort by depth descending, take top N\n results.sort((a, b) => b.depth - a.depth);\n return results.slice(0, limit);\n}\n"],"mappings":";;;;;AAUO,SAAS,WACd,IACA,OAA8D,CAAC,GAC5C;AACnB,QAAM,EAAE,QAAQ,IAAI,OAAO,WAAW,EAAE,IAAI;AAC5C,QAAM,QAAQ,kBAAkB,IAAI,KAAK;AAGzC,QAAM,UAA6B,CAAC;AAEpC,WAAS,IAAI,MAAc,MAAgB,SAA4B;AACrE,UAAM,YAAY,MAAM,IAAI,IAAI;AAChC,QAAI,CAAC,aAAa,UAAU,SAAS,GAAG;AACtC,UAAI,KAAK,UAAU,UAAU;AAC3B,gBAAQ,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,KAAK,OAAO,CAAC;AAAA,MACvD;AACA;AAAA,IACF;AAEA,QAAI,WAAW;AACf,eAAW,QAAQ,WAAW;AAC5B,UAAI,QAAQ,IAAI,IAAI,EAAG;AACvB,cAAQ,IAAI,IAAI;AAChB,WAAK,KAAK,IAAI;AACd,UAAI,MAAM,MAAM,OAAO;AACvB,WAAK,IAAI;AACT,cAAQ,OAAO,IAAI;AACnB,iBAAW;AAAA,IACb;AAGA,QAAI,CAAC,YAAY,KAAK,UAAU,UAAU;AACxC,cAAQ,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,KAAK,OAAO,CAAC;AAAA,IACvD;AAAA,EACF;AAGA,aAAW,aAAa,MAAM,KAAK,GAAG;AACpC,UAAM,UAAU,oBAAI,IAAY,CAAC,SAAS,CAAC;AAC3C,QAAI,WAAW,CAAC,SAAS,GAAG,OAAO;AAGnC,QAAI,QAAQ,SAAS,QAAQ,GAAI;AAAA,EACnC;AAGA,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACxC,SAAO,QAAQ,MAAM,GAAG,KAAK;AAC/B;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/files.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { FileResult } from '../types.js';\n\nexport function files(db: ScipDatabase, pattern: string): FileResult[] {\n const rows = db.all<{ relative_path: string }>(\n `SELECT relative_path FROM documents\n WHERE relative_path LIKE ?\n ORDER BY relative_path`,\n `%${pattern}%`,\n );\n\n // Apply gitignore filtering\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({ relativePath: r.relative_path }));\n}\n"],"mappings":";AAGO,SAAS,MAAM,IAAkB,SAA+B;AACrE,QAAM,OAAO,GAAG;AAAA,IACd;AAAA;AAAA;AAAA,IAGA,IAAI,OAAO;AAAA,EACb;AAGA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE;AACnD;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/deps.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { DepResult } from '../types.js';\n\n/** What internal files does this file depend on? (forward dependencies) */\nexport function deps(db: ScipDatabase, filePattern: string): DepResult[] {\n const rows = db.all<{ relative_path: string }>(\n `SELECT DISTINCT d2.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d1 ON c.document_id = d1.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d2 ON der.document_id = d2.id\n WHERE d1.relative_path LIKE ?\n AND d2.relative_path <> d1.relative_path\n AND ${db.localSymbolPredicate}\n ORDER BY d2.relative_path`,\n `%${filePattern}%`,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({ relativePath: r.relative_path }));\n}\n\n/** What files depend on this file/module? (reverse dependencies) */\nexport function rdeps(db: ScipDatabase, filePattern: string): DepResult[] {\n const rows = db.all<{ relative_path: string }>(\n `SELECT DISTINCT d1.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d1 ON c.document_id = d1.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d2 ON der.document_id = d2.id\n WHERE d2.relative_path LIKE ?\n AND d1.relative_path NOT LIKE ?\n ORDER BY d1.relative_path`,\n `%${filePattern}%`,\n `%${filePattern}%`,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({ relativePath: r.relative_path }));\n}\n"],"mappings":";AAIO,SAAS,KAAK,IAAkB,aAAkC;AACvE,QAAM,OAAO,GAAG;AAAA,IACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YASQ,GAAG,oBAAoB;AAAA;AAAA,IAE/B,IAAI,WAAW;AAAA,EACjB;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE;AACnD;AAGO,SAAS,MAAM,IAAkB,aAAkC;AACxE,QAAM,OAAO,GAAG;AAAA,IACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUA,IAAI,WAAW;AAAA,IACf,IAAI,WAAW;AAAA,EACjB;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE;AACnD;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/passthrough-candidates.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { getCalleeRowsForSymbol, testFileExclusionSql } from '../query-support.js';\nimport type { PassthroughCandidate } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find passthrough candidates: functions that just forward to one\n * other function.\n *\n * A function with exactly 1 callee and small LOC is likely a thin\n * wrapper that adds no value — it just passes arguments through to\n * the real implementation.\n */\nexport function passthroughCandidates(\n db: ScipDatabase,\n opts?: { scope?: string; maxLoc?: number; limit?: number },\n): PassthroughCandidate[] {\n const { scope, maxLoc = 15, limit = 30 } = opts ?? {};\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n // 1. Find all non-trivial symbols with definition ranges (>= 3 LOC)\n const symbols = db.all<{\n id: number;\n symbol: string;\n document_id: number;\n start_line: number;\n end_line: number;\n loc: number;\n relative_path: string;\n }>(\n `SELECT\n gs.id,\n gs.symbol,\n der.document_id,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n d.relative_path\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testFileExclusionSql('d')}\n ${db.symbolNoiseFor('gs')}\n AND (der.end_line - der.start_line + 1) >= 3\n AND (der.end_line - der.start_line + 1) <= ?\n ${scopeFilter}\n ORDER BY d.relative_path`,\n maxLoc,\n );\n\n const results: PassthroughCandidate[] = [];\n\n for (const sym of symbols) {\n if (db.isIgnored(sym.relative_path)) continue;\n\n // 2. Count callees for this symbol\n const callees = getCalleeRowsForSymbol(db, {\n documentId: sym.document_id,\n startLine: sym.start_line,\n endLine: sym.end_line,\n symbolId: sym.id,\n });\n\n // Deduplicate by symbol (same callee may appear in multiple chunks)\n const uniqueCallees = new Map<string, { symbol: string; file: string }>();\n for (const c of callees) {\n if (!uniqueCallees.has(c.symbol)) {\n uniqueCallees.set(c.symbol, { symbol: c.symbol, file: c.file });\n }\n }\n\n // 3. Passthrough = exactly 1 unique callee\n if (uniqueCallees.size !== 1) continue;\n\n const [, callee] = [...uniqueCallees.entries()][0]!;\n\n results.push({\n symbol: sym.symbol,\n shortName: shortenSymbol(sym.symbol),\n file: sym.relative_path,\n startLine: sym.start_line,\n endLine: sym.end_line,\n loc: sym.loc,\n forwardsTo: callee.symbol,\n forwardsToShort: shortenSymbol(callee.symbol),\n forwardsToFile: callee.file,\n });\n }\n\n results.sort((a, b) => a.loc - b.loc || a.file.localeCompare(b.file));\n return results.slice(0, limit);\n}\n"],"mappings":";;;;;;;;;AAaO,SAAS,sBACd,IACA,MACwB;AACxB,QAAM,EAAE,OAAO,SAAS,IAAI,QAAQ,GAAG,IAAI,QAAQ,CAAC;AACpD,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAGtE,QAAM,UAAU,GAAG;AAAA,IASjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAYI,GAAG,kBAAkB,GAAG,CAAC;AAAA,YACrB,qBAAqB,GAAG,CAAC;AAAA,QAC7B,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA,QAGvB,WAAW;AAAA;AAAA,IAEf;AAAA,EACF;AAEA,QAAM,UAAkC,CAAC;AAEzC,aAAW,OAAO,SAAS;AACzB,QAAI,GAAG,UAAU,IAAI,aAAa,EAAG;AAGrC,UAAM,UAAU,uBAAuB,IAAI;AAAA,MACzC,YAAY,IAAI;AAAA,MAChB,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,MACb,UAAU,IAAI;AAAA,IAChB,CAAC;AAGD,UAAM,gBAAgB,oBAAI,IAA8C;AACxE,eAAW,KAAK,SAAS;AACvB,UAAI,CAAC,cAAc,IAAI,EAAE,MAAM,GAAG;AAChC,sBAAc,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,MAAM,EAAE,KAAK,CAAC;AAAA,MAChE;AAAA,IACF;AAGA,QAAI,cAAc,SAAS,EAAG;AAE9B,UAAM,CAAC,EAAE,MAAM,IAAI,CAAC,GAAG,cAAc,QAAQ,CAAC,EAAE,CAAC;AAEjD,YAAQ,KAAK;AAAA,MACX,QAAQ,IAAI;AAAA,MACZ,WAAW,cAAc,IAAI,MAAM;AAAA,MACnC,MAAM,IAAI;AAAA,MACV,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,MACb,KAAK,IAAI;AAAA,MACT,YAAY,OAAO;AAAA,MACnB,iBAAiB,cAAc,OAAO,MAAM;AAAA,MAC5C,gBAAgB,OAAO;AAAA,IACzB,CAAC;AAAA,EACH;AAEA,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACpE,SAAO,QAAQ,MAAM,GAAG,KAAK;AAC/B;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/methods.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { MethodResult } from '../types.js';\nimport { leafName } from '../symbol-parser.js';\n\nexport function methods(db: ScipDatabase, className: string): MethodResult[] {\n const rows = db.all<{\n start_line: number;\n end_line: number;\n symbol: string;\n }>(\n `SELECT der.start_line, der.end_line, gs.symbol\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n WHERE gs.symbol LIKE ?\n AND ${db.localSymbolPredicate}\n AND gs.symbol LIKE '%().%'\n ${db.symbolNoise}\n ORDER BY der.start_line`,\n `%${className}#%`,\n );\n\n return rows.map((r) => ({\n startLine: r.start_line,\n endLine: r.end_line,\n name: leafName(r.symbol),\n }));\n}\n"],"mappings":";;;;;AAIO,SAAS,QAAQ,IAAkB,WAAmC;AAC3E,QAAM,OAAO,GAAG;AAAA,IAKd;AAAA;AAAA;AAAA;AAAA,YAIQ,GAAG,oBAAoB;AAAA;AAAA,QAE3B,GAAG,WAAW;AAAA;AAAA,IAElB,IAAI,SAAS;AAAA,EACf;AAEA,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,MAAM,SAAS,EAAE,MAAM;AAAA,EACzB,EAAE;AACJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/symbol-parser.ts"],"sourcesContent":["import type { ScipSymbol, ScipDescriptor, ScipLocalSymbol, DescriptorSuffix } from './types.js';\n\n/**\n * SCIP Symbol Grammar (from the SCIP spec):\n *\n * <symbol> ::= <scheme> ' ' <package> ' ' <descriptor>+ | 'local ' <local-id>\n * <package> ::= <manager> ' ' <package-name> ' ' <version> ' '\n * <descriptor> ::= <name> <suffix>\n *\n * Suffix characters:\n * / namespace\n * # type (class, interface, enum)\n * . term (variable, field, property)\n * (). method\n * [ type parameter\n * () parameter\n * : meta\n * ! macro\n *\n * Names may be backtick-escaped: `some.weird/name`\n */\n\nconst SUFFIX_MAP: Record<string, DescriptorSuffix> = {\n '/': 'namespace',\n '#': 'type',\n '.': 'term',\n '[': 'type-param',\n ':': 'meta',\n '!': 'macro',\n};\n\n/**\n * Parse a SCIP symbol string into its structured components.\n * Works for any SCIP-indexed language (TypeScript, Java, Rust, Python, etc.)\n */\nexport function parseSymbol(raw: string): ScipSymbol | ScipLocalSymbol {\n if (raw.startsWith('local ')) {\n return { kind: 'local', id: raw.slice(6), raw };\n }\n\n // Split: <scheme> <manager> <package-name> <version> <descriptors...>\n // The tricky part: package-name can contain spaces if backtick-escaped\n const parts = raw.split(' ');\n if (parts.length < 4) {\n // Malformed — return a best-effort parse\n return {\n scheme: parts[0] ?? '',\n manager: parts[1] ?? '',\n packageName: parts[2] ?? '',\n version: '',\n descriptors: [],\n raw,\n };\n }\n\n const scheme = parts[0]!;\n const manager = parts[1]!;\n\n // Package name and version: package name might be backtick-escaped\n // After scheme + manager, we need to find package + version + descriptor string\n let restAfterManager = raw.slice(scheme.length + 1 + manager.length + 1);\n\n // Parse package name (may be backtick-escaped)\n let packageName: string;\n if (restAfterManager.startsWith('`')) {\n const closingTick = restAfterManager.indexOf('`', 1);\n if (closingTick === -1) {\n packageName = restAfterManager.slice(1);\n restAfterManager = '';\n } else {\n packageName = restAfterManager.slice(1, closingTick);\n restAfterManager = restAfterManager.slice(closingTick + 2); // skip ` and space\n }\n } else {\n const spaceIdx = restAfterManager.indexOf(' ');\n if (spaceIdx === -1) {\n packageName = restAfterManager;\n restAfterManager = '';\n } else {\n packageName = restAfterManager.slice(0, spaceIdx);\n restAfterManager = restAfterManager.slice(spaceIdx + 1);\n }\n }\n\n // Parse version\n let version: string;\n const versionSpaceIdx = restAfterManager.indexOf(' ');\n if (versionSpaceIdx === -1) {\n version = restAfterManager;\n restAfterManager = '';\n } else {\n version = restAfterManager.slice(0, versionSpaceIdx);\n restAfterManager = restAfterManager.slice(versionSpaceIdx + 1);\n }\n\n // Parse descriptors from the remaining string\n const descriptors = parseDescriptors(restAfterManager);\n\n return { scheme, manager, packageName, version, descriptors, raw };\n}\n\n/**\n * Parse the descriptor chain from a SCIP symbol.\n *\n * SCIP descriptor grammar:\n * namespace: name/\n * type: name#\n * term: name.\n * method: name(disambiguator).\n * type-param: [name] (bracket-wrapped, prefix syntax)\n * parameter: (name) (paren-wrapped, prefix syntax)\n * meta: name:\n * macro: name!\n *\n * Names can be backtick-escaped: `some/name.with.dots`\n */\nfunction parseDescriptors(input: string): ScipDescriptor[] {\n const descriptors: ScipDescriptor[] = [];\n let i = 0;\n\n while (i < input.length) {\n // Type parameter: [name]\n if (input[i] === '[') {\n const closeBracket = input.indexOf(']', i + 1);\n if (closeBracket === -1) {\n descriptors.push({ name: input.slice(i + 1), suffix: 'type-param' });\n break;\n }\n descriptors.push({ name: input.slice(i + 1, closeBracket), suffix: 'type-param' });\n i = closeBracket + 1;\n continue;\n }\n\n // Parameter: (name) — only when ( appears at descriptor start with no preceding name\n if (input[i] === '(' && (descriptors.length === 0 || i === 0 || isSuffixChar(input[i - 1]!))) {\n const closeParen = input.indexOf(')', i + 1);\n if (closeParen !== -1 && input[closeParen + 1] !== '.') {\n // This is a parameter (name), not a method disambiguator\n descriptors.push({ name: input.slice(i + 1, closeParen), suffix: 'parameter' });\n i = closeParen + 1;\n continue;\n }\n }\n\n let name: string;\n\n // Backtick-escaped name\n if (input[i] === '`') {\n const closingTick = input.indexOf('`', i + 1);\n if (closingTick === -1) {\n name = input.slice(i + 1);\n i = input.length;\n descriptors.push({ name, suffix: 'term' });\n break;\n }\n name = input.slice(i + 1, closingTick);\n i = closingTick + 1;\n } else {\n // Read name until we hit a suffix character\n const start = i;\n while (i < input.length && !isSuffixChar(input[i]!)) {\n i++;\n }\n name = input.slice(start, i);\n }\n\n // Parse suffix after name\n if (i >= input.length) {\n if (name) descriptors.push({ name, suffix: 'term' });\n break;\n }\n\n const char = input[i]!;\n\n // Method: name(disambiguator).\n if (char === '(') {\n const closeParen = input.indexOf(')', i + 1);\n if (closeParen !== -1 && input[closeParen + 1] === '.') {\n descriptors.push({ name, suffix: 'method' });\n i = closeParen + 2; // skip past ).\n } else if (closeParen !== -1) {\n // Bare (disambiguator) without . — treat as method anyway (common in practice)\n descriptors.push({ name, suffix: 'method' });\n i = closeParen + 1;\n } else {\n descriptors.push({ name, suffix: 'term' });\n i++;\n }\n } else {\n const suffix = SUFFIX_MAP[char];\n if (suffix) {\n descriptors.push({ name, suffix });\n i += 1;\n } else {\n i += 1; // Unknown suffix — skip\n }\n }\n }\n\n return descriptors;\n}\n\nfunction isSuffixChar(c: string): boolean {\n return c === '/' || c === '#' || c === '.' || c === '(' || c === '[' || c === ':' || c === '!';\n}\n\n/**\n * Convert a parsed SCIP symbol to a short, human-readable name.\n * Language-agnostic: works for any SCIP-indexed language.\n *\n * Examples:\n * \"scip-typescript npm @vega/api 0.1.3 src/modules/auth/auth.service.ts/AuthService#login().\"\n * → \"auth.service:AuthService:login()\"\n *\n * \"scip-java maven com.example/mylib 1.0.0 com/example/MyClass#doStuff().\"\n * → \"MyClass:doStuff()\"\n *\n * \"rust-analyzer cargo my-crate 0.1.0 src/lib.rs/MyStruct#new().\"\n * → \"lib:MyStruct:new()\"\n */\nexport function shortenSymbol(raw: string): string {\n const parsed = parseSymbol(raw);\n if ('kind' in parsed && parsed.kind === 'local') {\n return `local:${parsed.id}`;\n }\n\n const sym = parsed as ScipSymbol;\n if (sym.descriptors.length === 0) return sym.raw;\n\n const parts: string[] = [];\n for (const desc of sym.descriptors) {\n // Strip file extensions from namespace descriptors (the file path parts)\n let name = desc.name;\n if (desc.suffix === 'namespace') {\n // Remove common file extensions\n name = name\n .replace(/\\.(ts|tsx|js|jsx|mjs|cjs)$/, '')\n .replace(/\\.(py|pyi)$/, '')\n .replace(/\\.(rs)$/, '')\n .replace(/\\.(java|scala|kt|kts)$/, '')\n .replace(/\\.(rb)$/, '')\n .replace(/\\.(go)$/, '')\n .replace(/\\.(cs|vb)$/, '')\n .replace(/\\.(dart)$/, '')\n .replace(/\\.(php)$/, '')\n .replace(/\\.(c|cc|cpp|cxx|h|hpp)$/, '');\n }\n\n // Skip empty names (can happen with trailing suffixes)\n if (!name) continue;\n\n // For methods, append () for clarity\n if (desc.suffix === 'method') {\n parts.push(`${name}()`);\n } else {\n parts.push(name);\n }\n }\n\n return parts.join(':');\n}\n\n/**\n * Extract just the leaf name from a SCIP symbol.\n * Useful when you only need the function/class/variable name without path context.\n */\nexport function leafName(raw: string): string {\n const parsed = parseSymbol(raw);\n if ('kind' in parsed && parsed.kind === 'local') {\n return parsed.id;\n }\n\n const sym = parsed as ScipSymbol;\n if (sym.descriptors.length === 0) return '';\n\n const last = sym.descriptors[sym.descriptors.length - 1]!;\n return last.name;\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/similar-signatures.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { SimilarSignatureGroup } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find functions with near-identical type signatures (same parameter types\n * and return type) but different names. These are \"same shape\" functions\n * that may be doing similar work even if their internal implementation differs.\n *\n * The SCIP `documentation` field often contains the full type signature\n * after a `|` delimiter. We parse it, normalize it (strip the function name,\n * whitespace, and case), then group by normalized signature.\n *\n * Groups with 2+ functions = same-shape candidates.\n */\nexport function similarSignatures(\n db: ScipDatabase,\n opts: { scope?: string; minLoc?: number; limit?: number } = {},\n): SimilarSignatureGroup[] {\n const { scope, minLoc = 1, limit } = opts;\n\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n // Get all function-level symbols with their documentation/signature strings.\n // We use the same signature extraction pattern as symbols.ts / trace.ts.\n // Filter to symbols that have a documentation field containing '|' (the sig delimiter)\n // and whose signature contains '(' (indicating a callable).\n const rows = db.all<{\n symbol: string;\n relative_path: string;\n start_line: number;\n end_line: number;\n loc: number;\n sig: string;\n }>(\n `SELECT\n gs.symbol,\n d.relative_path,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE gs.documentation IS NOT NULL\n AND gs.documentation != ''\n AND INSTR(gs.documentation, '|') > 0\n AND (der.end_line - der.start_line + 1) >= ?\n ${db.pathExclusionsFor('d')}\n ${db.symbolNoiseFor('gs')}\n ${scopeFilter}\n ORDER BY d.relative_path, der.start_line`,\n minLoc,\n );\n\n // Group by normalized signature\n const sigGroups = new Map<string, Array<{\n symbol: string;\n shortName: string;\n file: string;\n startLine: number;\n endLine: number;\n loc: number;\n }>>();\n\n for (const row of rows) {\n if (db.isIgnored(row.relative_path)) continue;\n\n const normalized = normalizeSignature(row.sig);\n if (!normalized) continue;\n\n const entry = {\n symbol: row.symbol,\n shortName: shortenSymbol(row.symbol),\n file: row.relative_path,\n startLine: row.start_line,\n endLine: row.end_line,\n loc: row.loc,\n };\n\n const existing = sigGroups.get(normalized);\n if (existing) {\n existing.push(entry);\n } else {\n sigGroups.set(normalized, [entry]);\n }\n }\n\n // Collect groups with 2+ functions\n const results: SimilarSignatureGroup[] = [];\n\n for (const [signature, functions] of sigGroups) {\n if (functions.length < 2) continue;\n\n results.push({ signature, functions });\n }\n\n // Sort by group size descending (largest groups = most duplication),\n // then by total LOC in the group\n results.sort((a, b) => {\n const sizeDiff = b.functions.length - a.functions.length;\n if (sizeDiff !== 0) return sizeDiff;\n const locA = a.functions.reduce((sum, f) => sum + f.loc, 0);\n const locB = b.functions.reduce((sum, f) => sum + f.loc, 0);\n return locB - locA;\n });\n\n return limit ? results.slice(0, limit) : results;\n}\n\n/**\n * Normalize a signature for comparison:\n * 1. Clean markdown fences and SCIP prefixes\n * 2. Strip everything before the first '(' (removes the function name)\n * 3. Strip whitespace and lowercase\n *\n * Returns null if the signature doesn't contain a callable form.\n */\nfunction normalizeSignature(raw: string): string | null {\n if (!raw || !raw.trim()) return null;\n\n // Clean markdown and SCIP decoration (same as cleanSignature)\n let sig = raw\n .replace(/^```\\w*\\s*/, '')\n .replace(/\\s*```$/, '')\n .replace(/^\\(method\\)\\s*/, '')\n .replace(/^\\(property\\)\\s*/, '')\n .replace(/^\\(function\\)\\s*/, '')\n .replace(/^\\(class\\)\\s*/, '')\n .replace(/^\\(interface\\)\\s*/, '')\n .replace(/^\\(enum\\)\\s*/, '')\n .replace(/^\\(type alias\\)\\s*/, '')\n .replace(/^\\(const\\)\\s*/, '')\n .replace(/^\\(var\\)\\s*/, '')\n .trim();\n\n // Find the first '(' — everything from there is the parameter/return signature\n const parenIdx = sig.indexOf('(');\n if (parenIdx === -1) return null;\n\n sig = sig.slice(parenIdx);\n\n // Normalize: strip all whitespace, lowercase\n sig = sig.replace(/\\s+/g, '').toLowerCase();\n\n // Must have meaningful content after normalization\n if (sig.length < 3) return null; // e.g. \"()\" alone is too generic\n\n return sig;\n}\n"],"mappings":";;;;;AAeO,SAAS,kBACd,IACA,OAA4D,CAAC,GACpC;AACzB,QAAM,EAAE,OAAO,SAAS,GAAG,MAAM,IAAI;AAErC,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAMtE,QAAM,OAAO,GAAG;AAAA,IAQd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAcI,GAAG,kBAAkB,GAAG,CAAC;AAAA,QACzB,GAAG,eAAe,IAAI,CAAC;AAAA,QACvB,WAAW;AAAA;AAAA,IAEf;AAAA,EACF;AAGA,QAAM,YAAY,oBAAI,IAOlB;AAEJ,aAAW,OAAO,MAAM;AACtB,QAAI,GAAG,UAAU,IAAI,aAAa,EAAG;AAErC,UAAM,aAAa,mBAAmB,IAAI,GAAG;AAC7C,QAAI,CAAC,WAAY;AAEjB,UAAM,QAAQ;AAAA,MACZ,QAAQ,IAAI;AAAA,MACZ,WAAW,cAAc,IAAI,MAAM;AAAA,MACnC,MAAM,IAAI;AAAA,MACV,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,MACb,KAAK,IAAI;AAAA,IACX;AAEA,UAAM,WAAW,UAAU,IAAI,UAAU;AACzC,QAAI,UAAU;AACZ,eAAS,KAAK,KAAK;AAAA,IACrB,OAAO;AACL,gBAAU,IAAI,YAAY,CAAC,KAAK,CAAC;AAAA,IACnC;AAAA,EACF;AAGA,QAAM,UAAmC,CAAC;AAE1C,aAAW,CAAC,WAAW,SAAS,KAAK,WAAW;AAC9C,QAAI,UAAU,SAAS,EAAG;AAE1B,YAAQ,KAAK,EAAE,WAAW,UAAU,CAAC;AAAA,EACvC;AAIA,UAAQ,KAAK,CAAC,GAAG,MAAM;AACrB,UAAM,WAAW,EAAE,UAAU,SAAS,EAAE,UAAU;AAClD,QAAI,aAAa,EAAG,QAAO;AAC3B,UAAM,OAAO,EAAE,UAAU,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAC1D,UAAM,OAAO,EAAE,UAAU,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAC1D,WAAO,OAAO;AAAA,EAChB,CAAC;AAED,SAAO,QAAQ,QAAQ,MAAM,GAAG,KAAK,IAAI;AAC3C;AAUA,SAAS,mBAAmB,KAA4B;AACtD,MAAI,CAAC,OAAO,CAAC,IAAI,KAAK,EAAG,QAAO;AAGhC,MAAI,MAAM,IACP,QAAQ,cAAc,EAAE,EACxB,QAAQ,WAAW,EAAE,EACrB,QAAQ,kBAAkB,EAAE,EAC5B,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,qBAAqB,EAAE,EAC/B,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,sBAAsB,EAAE,EAChC,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,eAAe,EAAE,EACzB,KAAK;AAGR,QAAM,WAAW,IAAI,QAAQ,GAAG;AAChC,MAAI,aAAa,GAAI,QAAO;AAE5B,QAAM,IAAI,MAAM,QAAQ;AAGxB,QAAM,IAAI,QAAQ,QAAQ,EAAE,EAAE,YAAY;AAG1C,MAAI,IAAI,SAAS,EAAG,QAAO;AAE3B,SAAO;AACT;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/dataflow.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch } from '../query-support.js';\nimport type { DataflowResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Reference-level dataflow analysis: where does data around this symbol\n * come from and where does it go?\n *\n * This is not value-level dataflow (we can't trace x = foo(); bar(x);\n * as a chain). Instead it shows:\n * - Where the symbol is defined and used\n * - What other symbols appear in the same enclosing scope (co-occurring data)\n * - What feeds into the function that defines it (producers)\n * - What consumes the function that uses it (consumers)\n *\n * Language-agnostic: works with any SCIP index.\n */\nexport function dataflow(\n db: ScipDatabase,\n symbolPattern: string,\n): DataflowResult | null {\n const match = findFirstSymbolMatch(db, symbolPattern);\n if (!match) return null;\n\n // Definition sites (role=1)\n const defSites = db.all<{ file: string; line: number }>(\n `SELECT d.relative_path AS file, c.start_line AS line\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = ? AND m.role = 1\n ORDER BY d.relative_path, c.start_line`,\n match.symbolId,\n );\n\n // Usage sites (role=0) with enclosing symbol\n const usageSites = db.all<{\n file: string;\n line: number;\n enclosing_symbol: string | null;\n }>(\n `SELECT d.relative_path AS file, c.start_line AS line,\n (SELECT enc_gs.symbol\n FROM defn_enclosing_ranges enc_der\n JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id\n WHERE enc_der.document_id = d.id\n AND enc_der.start_line <= c.start_line\n AND enc_der.end_line >= c.end_line\n ORDER BY (enc_der.end_line - enc_der.start_line) ASC\n LIMIT 1\n ) AS enclosing_symbol\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = ? AND m.role = 0\n ${db.pathExclusionsFor('d')}\n ORDER BY d.relative_path, c.start_line`,\n match.symbolId,\n );\n\n // Producers: other symbols referenced within the same function that defines our target\n const producers = db.all<{ symbol: string; file: string }>(\n `SELECT DISTINCT other_gs.symbol, other_d.relative_path AS file\n FROM mentions other_m\n JOIN chunks other_c ON other_m.chunk_id = other_c.id\n JOIN global_symbols other_gs ON other_m.symbol_id = other_gs.id\n JOIN defn_enclosing_ranges other_der ON other_gs.id = other_der.symbol_id\n JOIN documents other_d ON other_der.document_id = other_d.id\n WHERE other_c.document_id = ?\n AND other_c.start_line >= ? AND other_c.end_line <= ?\n AND other_m.role = 0\n AND other_gs.id != ?\n ${db.symbolNoiseFor('other_gs')}\n ${db.pathExclusionsFor('other_d')}\n ORDER BY other_d.relative_path\n LIMIT 30`,\n match.documentId, match.startLine, match.endLine, match.symbolId,\n );\n\n // Consumers: symbols exported/defined by functions that reference our target\n // (what does the data flow into after being used)\n const consumers = db.all<{ symbol: string; file: string }>(\n `SELECT DISTINCT consumer_gs.symbol, consumer_d.relative_path AS file\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN documents ref_d ON ref_c.document_id = ref_d.id\n -- Find the enclosing function at each usage site\n JOIN defn_enclosing_ranges enc_der\n ON enc_der.document_id = ref_d.id\n AND enc_der.start_line <= ref_c.start_line\n AND enc_der.end_line >= ref_c.end_line\n JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id\n -- Find other symbols defined by that enclosing function's file\n JOIN mentions consumer_m ON consumer_m.symbol_id = enc_gs.id AND consumer_m.role = 0\n JOIN chunks consumer_c ON consumer_m.chunk_id = consumer_c.id\n JOIN documents consumer_d ON consumer_c.document_id = consumer_d.id\n JOIN global_symbols consumer_gs ON consumer_m.symbol_id = consumer_gs.id\n WHERE ref_m.symbol_id = ? AND ref_m.role = 0\n AND consumer_d.id != ref_d.id\n ${db.symbolNoiseFor('consumer_gs')}\n ${db.pathExclusionsFor('consumer_d')}\n ORDER BY consumer_d.relative_path\n LIMIT 30`,\n match.symbolId,\n );\n\n return {\n symbol: match.symbol,\n shortName: shortenSymbol(match.symbol),\n relativePath: match.relativePath,\n definitionSites: defSites.filter((s) => !db.isIgnored(s.file)),\n usageSites: usageSites\n .filter((s) => !db.isIgnored(s.file))\n .map((s) => ({\n file: s.file,\n line: s.line,\n enclosingSymbol: s.enclosing_symbol ?? '(top-level)',\n enclosingShort: s.enclosing_symbol ? shortenSymbol(s.enclosing_symbol) : '(top-level)',\n })),\n producers: producers\n .filter((p) => !db.isIgnored(p.file))\n .map((p) => ({ symbol: p.symbol, shortName: shortenSymbol(p.symbol), file: p.file })),\n consumers: consumers\n .filter((c) => !db.isIgnored(c.file))\n .map((c) => ({ symbol: c.symbol, shortName: shortenSymbol(c.symbol), file: c.file })),\n };\n}\n"],"mappings":";;;;;;;;AAkBO,SAAS,SACd,IACA,eACuB;AACvB,QAAM,QAAQ,qBAAqB,IAAI,aAAa;AACpD,MAAI,CAAC,MAAO,QAAO;AAGnB,QAAM,WAAW,GAAG;AAAA,IAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM;AAAA,EACR;AAGA,QAAM,aAAa,GAAG;AAAA,IAKpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAcI,GAAG,kBAAkB,GAAG,CAAC;AAAA;AAAA,IAE7B,MAAM;AAAA,EACR;AAGA,QAAM,YAAY,GAAG;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUI,GAAG,eAAe,UAAU,CAAC;AAAA,QAC7B,GAAG,kBAAkB,SAAS,CAAC;AAAA;AAAA;AAAA,IAGnC,MAAM;AAAA,IAAY,MAAM;AAAA,IAAW,MAAM;AAAA,IAAS,MAAM;AAAA,EAC1D;AAIA,QAAM,YAAY,GAAG;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAiBI,GAAG,eAAe,aAAa,CAAC;AAAA,QAChC,GAAG,kBAAkB,YAAY,CAAC;AAAA;AAAA;AAAA,IAGtC,MAAM;AAAA,EACR;AAEA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,WAAW,cAAc,MAAM,MAAM;AAAA,IACrC,cAAc,MAAM;AAAA,IACpB,iBAAiB,SAAS,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC;AAAA,IAC7D,YAAY,WACT,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO;AAAA,MACX,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,MACR,iBAAiB,EAAE,oBAAoB;AAAA,MACvC,gBAAgB,EAAE,mBAAmB,cAAc,EAAE,gBAAgB,IAAI;AAAA,IAC3E,EAAE;AAAA,IACJ,WAAW,UACR,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,WAAW,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE;AAAA,IACtF,WAAW,UACR,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,WAAW,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE;AAAA,EACxF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/trace.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { TraceResult } from '../types.js';\nimport { cleanSignature } from './clean-signature.js';\n\nexport function trace(db: ScipDatabase, symbolPattern: string): TraceResult {\n // Definitions\n const defRows = db.all<{\n relative_path: string;\n start_line: number;\n end_line: number;\n sig: string | null;\n }>(\n `SELECT d.relative_path, der.start_line, der.end_line,\n REPLACE(SUBSTR(gs.documentation, INSTR(gs.documentation, '|') + 1), char(10), ' ') AS sig\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE gs.symbol LIKE ?\n AND ${db.localSymbolPredicate}\n ${db.symbolNoise}\n ORDER BY d.relative_path, der.start_line\n LIMIT 10`,\n `%${symbolPattern}%`,\n );\n\n const definitions = defRows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n relativePath: r.relative_path,\n startLine: r.start_line,\n endLine: r.end_line,\n signature: cleanSignature(r.sig),\n }));\n\n // References\n const refRows = db.all<{ relative_path: string }>(\n `SELECT DISTINCT d.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n WHERE gs.symbol LIKE ?\n AND ${db.localSymbolPredicate}\n ${db.symbolNoise}\n AND m.role = 0\n ORDER BY d.relative_path`,\n `%${symbolPattern}%`,\n );\n\n const referencedBy = refRows\n .map((r) => r.relative_path)\n .filter((p) => !db.isIgnored(p));\n\n return { definitions, referencedBy };\n}\n"],"mappings":";;;;;AAIO,SAAS,MAAM,IAAkB,eAAoC;AAE1E,QAAM,UAAU,GAAG;AAAA,IAMjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAMQ,GAAG,oBAAoB;AAAA,QAC3B,GAAG,WAAW;AAAA;AAAA;AAAA,IAGlB,IAAI,aAAa;AAAA,EACnB;AAEA,QAAM,cAAc,QACjB,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,cAAc,EAAE;AAAA,IAChB,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,WAAW,eAAe,EAAE,GAAG;AAAA,EACjC,EAAE;AAGJ,QAAM,UAAU,GAAG;AAAA,IACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAMQ,GAAG,oBAAoB;AAAA,QAC3B,GAAG,WAAW;AAAA;AAAA;AAAA,IAGlB,IAAI,aAAa;AAAA,EACnB;AAEA,QAAM,eAAe,QAClB,IAAI,CAAC,MAAM,EAAE,aAAa,EAC1B,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC;AAEjC,SAAO,EAAE,aAAa,aAAa;AACrC;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/wrapper-candidates.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { testFileExclusionSql } from '../query-support.js';\nimport type { WrapperCandidate } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find wrapper candidates: symbols called by only one other symbol.\n *\n * These are premature abstractions that add indirection without\n * providing reuse. A function with fan-in = 1 whose sole caller\n * is widely used is a strong signal of unnecessary wrapping.\n */\nexport function wrapperCandidates(\n db: ScipDatabase,\n opts?: { scope?: string; maxLoc?: number; limit?: number },\n): WrapperCandidate[] {\n const { scope, maxLoc = 15, limit = 30 } = opts ?? {};\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n // Find all symbols with exactly 1 cross-file consumer (fan-in = 1),\n // along with who that single caller is and the caller's own fan-in.\n const rows = db.all<{\n symbol: string;\n file: string;\n start_line: number;\n end_line: number;\n loc: number;\n caller_symbol: string;\n caller_fan_in: number;\n }>(\n `SELECT * FROM (\n SELECT\n gs.symbol,\n d.relative_path AS file,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n -- The single caller: the symbol whose definition range contains\n -- the chunk that references our target\n (SELECT caller_gs.symbol\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN defn_enclosing_ranges caller_der\n ON caller_der.document_id = ref_c.document_id\n AND ref_c.start_line >= caller_der.start_line\n AND ref_c.end_line <= caller_der.end_line\n JOIN global_symbols caller_gs ON caller_der.symbol_id = caller_gs.id\n WHERE ref_m.symbol_id = gs.id\n AND ref_m.role = 0\n AND ref_c.document_id != der.document_id\n LIMIT 1\n ) AS caller_symbol,\n -- Fan-in of that single caller\n (SELECT COUNT(DISTINCT caller_ref_c.document_id)\n FROM mentions caller_ref_m\n JOIN chunks caller_ref_c ON caller_ref_m.chunk_id = caller_ref_c.id\n WHERE caller_ref_m.symbol_id = (\n SELECT caller_der2.symbol_id\n FROM mentions ref_m2\n JOIN chunks ref_c2 ON ref_m2.chunk_id = ref_c2.id\n JOIN defn_enclosing_ranges caller_der2\n ON caller_der2.document_id = ref_c2.document_id\n AND ref_c2.start_line >= caller_der2.start_line\n AND ref_c2.end_line <= caller_der2.end_line\n WHERE ref_m2.symbol_id = gs.id\n AND ref_m2.role = 0\n AND ref_c2.document_id != der.document_id\n LIMIT 1\n )\n AND caller_ref_m.role = 0\n ) AS caller_fan_in\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testFileExclusionSql('d')}\n ${db.symbolNoiseFor('gs')}\n -- Only functions/terms, not type definitions (types with # are not wrappers)\n AND gs.symbol NOT LIKE '%#'\n AND (der.end_line - der.start_line + 1) <= ?\n AND (der.end_line - der.start_line + 1) >= 2\n ${scopeFilter}\n -- Exactly 1 cross-file consumer\n AND (\n SELECT COUNT(DISTINCT ref_c.document_id)\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n WHERE ref_m.symbol_id = gs.id\n AND ref_m.role = 0\n AND ref_c.document_id != der.document_id\n ) = 1\n ) WHERE caller_symbol IS NOT NULL AND caller_fan_in > 3\n ORDER BY caller_fan_in DESC, loc DESC\n LIMIT ?`,\n maxLoc, limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.file))\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n file: r.file,\n startLine: r.start_line,\n endLine: r.end_line,\n loc: r.loc,\n singleCaller: r.caller_symbol,\n singleCallerShort: shortenSymbol(r.caller_symbol),\n callerFanIn: r.caller_fan_in,\n }));\n}\n"],"mappings":";;;;;;;;AAYO,SAAS,kBACd,IACA,MACoB;AACpB,QAAM,EAAE,OAAO,SAAS,IAAI,QAAQ,GAAG,IAAI,QAAQ,CAAC;AACpD,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAItE,QAAM,OAAO,GAAG;AAAA,IASd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UA6CM,GAAG,kBAAkB,GAAG,CAAC;AAAA,cACrB,qBAAqB,GAAG,CAAC;AAAA,UAC7B,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,UAKvB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAajB;AAAA,IAAQ;AAAA,EACV;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,MAAM,EAAE;AAAA,IACR,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,KAAK,EAAE;AAAA,IACP,cAAc,EAAE;AAAA,IAChB,mBAAmB,cAAc,EAAE,aAAa;AAAA,IAChD,aAAa,EAAE;AAAA,EACjB,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/similar.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch, getCalleeRowsForSymbol } from '../query-support.js';\nimport type { SimilarSymbolResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find functions with similar callee fingerprints using TF-IDF weighted\n * cosine similarity.\n *\n * Plain Jaccard similarity inflates scores when functions share ubiquitous\n * infrastructure imports (db, types, shortenSymbol). TF-IDF fixes this by\n * weighting each shared callee by how rare it is:\n *\n * - A callee used by 2 functions (rare) gets high weight → strong signal\n * - A callee used by 30 functions (common) gets low weight → noise\n *\n * This means two functions sharing `sendWelcomeEmail()` (rare) score\n * much higher than two functions sharing `db.all()` (ubiquitous), even\n * though both are \"shared callees.\"\n */\nexport function similar(\n db: ScipDatabase,\n symbolPattern: string,\n opts: { minSimilarity?: number; limit?: number } = {},\n): SimilarSymbolResult[] {\n const { minSimilarity = 0.4, limit = 20 } = opts;\n\n const target = findCallees(db, symbolPattern);\n if (!target || target.callees.size === 0) return [];\n\n const candidates = getAllCalleeFingerprints(db, {\n minCallees: 3,\n excludeSymbol: target.symbol,\n });\n\n // Compute IDF weights across all fingerprints + target\n const allFingerprints = [target, ...candidates];\n const idfWeights = computeIdf(allFingerprints);\n\n const results: SimilarSymbolResult[] = [];\n\n for (const candidate of candidates) {\n if (candidate.callees.size < 3) continue;\n\n const { similarity, significantShared, trivialShared } = weightedSimilarity(\n target.callees, candidate.callees, idfWeights,\n );\n\n if (similarity < minSimilarity) continue;\n if (significantShared.length < 1) continue; // no real overlap\n\n results.push({\n symbolA: target.symbol,\n shortNameA: shortenSymbol(target.symbol),\n fileA: target.file,\n symbolB: candidate.symbol,\n shortNameB: shortenSymbol(candidate.symbol),\n fileB: candidate.file,\n similarity,\n sharedCallees: significantShared.map(shortenSymbol),\n uniqueToA: [...difference(target.callees, candidate.callees)].map(shortenSymbol),\n uniqueToB: [...difference(candidate.callees, target.callees)].map(shortenSymbol),\n });\n }\n\n results.sort((a, b) => b.similarity - a.similarity);\n return results.slice(0, limit);\n}\n\n/**\n * Find similar symbols across the entire codebase.\n * Uses TF-IDF weighted similarity to filter out infrastructure noise.\n */\nexport function similarAll(\n db: ScipDatabase,\n opts: { minSimilarity?: number; limit?: number; scope?: string; minCallees?: number } = {},\n): SimilarSymbolResult[] {\n const { minSimilarity = 0.5, limit = 20, scope, minCallees = 4 } = opts;\n\n const all = getAllCalleeFingerprints(db, { minCallees, scope });\n const idfWeights = computeIdf(all);\n\n const results: SimilarSymbolResult[] = [];\n\n for (let i = 0; i < all.length; i++) {\n for (let j = i + 1; j < all.length; j++) {\n const a = all[i]!;\n const b = all[j]!;\n\n if (a.file === b.file) continue;\n\n const { similarity, significantShared } = weightedSimilarity(\n a.callees, b.callees, idfWeights,\n );\n\n if (similarity < minSimilarity) continue;\n if (significantShared.length < 2) continue;\n\n results.push({\n symbolA: a.symbol,\n shortNameA: shortenSymbol(a.symbol),\n fileA: a.file,\n symbolB: b.symbol,\n shortNameB: shortenSymbol(b.symbol),\n fileB: b.file,\n similarity,\n sharedCallees: significantShared.map(shortenSymbol),\n uniqueToA: [...difference(a.callees, b.callees)].map(shortenSymbol),\n uniqueToB: [...difference(b.callees, a.callees)].map(shortenSymbol),\n });\n }\n\n if (results.length > limit * 5) break;\n }\n\n results.sort((a, b) => b.similarity - a.similarity);\n return results.slice(0, limit);\n}\n\n// ── TF-IDF Engine ──────────────────────────────────────────\n\n/**\n * Compute inverse document frequency for each callee.\n * IDF(callee) = log(N / df(callee)) where N is total functions\n * and df is how many functions reference that callee.\n *\n * High IDF = rare callee = strong similarity signal.\n * Low IDF = ubiquitous callee = noise.\n */\nfunction computeIdf(fingerprints: SymbolFingerprint[]): Map<string, number> {\n const n = fingerprints.length;\n if (n === 0) return new Map();\n\n // Count how many functions reference each callee\n const docFreq = new Map<string, number>();\n for (const fp of fingerprints) {\n for (const callee of fp.callees) {\n docFreq.set(callee, (docFreq.get(callee) ?? 0) + 1);\n }\n }\n\n // Compute IDF\n const idf = new Map<string, number>();\n for (const [callee, df] of docFreq) {\n idf.set(callee, Math.log(n / df));\n }\n\n return idf;\n}\n\n/**\n * Compute TF-IDF weighted cosine similarity between two callee sets.\n *\n * Each callee is a dimension. Its weight is its IDF score.\n * Cosine similarity of the weighted vectors gives a similarity\n * that ignores ubiquitous callees and emphasizes rare shared ones.\n *\n * Also returns which shared callees are \"significant\" (above-median IDF)\n * vs \"trivial\" (below-median IDF, i.e., infrastructure).\n */\nfunction weightedSimilarity(\n a: Set<string>,\n b: Set<string>,\n idf: Map<string, number>,\n): { similarity: number; significantShared: string[]; trivialShared: string[] } {\n const shared = intersection(a, b);\n if (shared.size === 0) return { similarity: 0, significantShared: [], trivialShared: [] };\n\n // Compute weighted dot product and magnitudes\n let dotProduct = 0;\n let magA = 0;\n let magB = 0;\n\n const allCallees = new Set([...a, ...b]);\n for (const callee of allCallees) {\n const weight = idf.get(callee) ?? 0;\n const inA = a.has(callee) ? weight : 0;\n const inB = b.has(callee) ? weight : 0;\n dotProduct += inA * inB;\n magA += inA * inA;\n magB += inB * inB;\n }\n\n const magnitude = Math.sqrt(magA) * Math.sqrt(magB);\n const similarity = magnitude > 0 ? dotProduct / magnitude : 0;\n\n // Split shared callees into significant (high IDF) and trivial (low IDF)\n const medianIdf = getMedianIdf(idf);\n const significantShared: string[] = [];\n const trivialShared: string[] = [];\n\n for (const callee of shared) {\n const weight = idf.get(callee) ?? 0;\n if (weight >= medianIdf) {\n significantShared.push(callee);\n } else {\n trivialShared.push(callee);\n }\n }\n\n // Sort significant callees by IDF descending (most distinctive first)\n significantShared.sort((x, y) => (idf.get(y) ?? 0) - (idf.get(x) ?? 0));\n\n return { similarity, significantShared, trivialShared };\n}\n\nfunction getMedianIdf(idf: Map<string, number>): number {\n const values = [...idf.values()].sort((a, b) => a - b);\n if (values.length === 0) return 0;\n const mid = Math.floor(values.length / 2);\n return values.length % 2 === 0\n ? (values[mid - 1]! + values[mid]!) / 2\n : values[mid]!;\n}\n\n// ── Internal helpers ───────────────────────────────────────\n\ninterface SymbolFingerprint {\n symbol: string;\n file: string;\n callees: Set<string>;\n}\n\nfunction findCallees(\n db: ScipDatabase,\n symbolPattern: string,\n): SymbolFingerprint | null {\n const target = findFirstSymbolMatch(db, symbolPattern);\n\n if (!target) return null;\n\n const calleeRows = getCalleeRowsForSymbol(db, target);\n\n return {\n symbol: target.symbol,\n file: target.relativePath,\n callees: new Set(calleeRows.map((r) => r.symbol)),\n };\n}\n\nfunction getAllCalleeFingerprints(\n db: ScipDatabase,\n opts: { minCallees: number; scope?: string; excludeSymbol?: string },\n): SymbolFingerprint[] {\n const { minCallees, scope, excludeSymbol } = opts;\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n const excludeFilter = excludeSymbol ? `AND gs.symbol != '${excludeSymbol.replace(/'/g, \"''\")}'` : '';\n\n const symbols = db.all<{\n id: number;\n symbol: string;\n document_id: number;\n start_line: number;\n end_line: number;\n relative_path: string;\n }>(\n `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n ${db.symbolNoiseFor('gs')}\n AND (der.end_line - der.start_line + 1) >= 5\n ${scopeFilter}\n ${excludeFilter}\n ORDER BY d.relative_path`,\n );\n\n const fingerprints: SymbolFingerprint[] = [];\n\n for (const sym of symbols) {\n if (db.isIgnored(sym.relative_path)) continue;\n\n const calleeRows = getCalleeRowsForSymbol(db, {\n documentId: sym.document_id,\n startLine: sym.start_line,\n endLine: sym.end_line,\n symbolId: sym.id,\n });\n\n const callees = new Set(calleeRows.map((r) => r.symbol));\n if (callees.size >= minCallees) {\n fingerprints.push({ symbol: sym.symbol, file: sym.relative_path, callees });\n }\n }\n\n return fingerprints;\n}\n\nfunction intersection<T>(a: Set<T>, b: Set<T>): Set<T> {\n const result = new Set<T>();\n for (const item of a) {\n if (b.has(item)) result.add(item);\n }\n return result;\n}\n\nfunction difference<T>(a: Set<T>, b: Set<T>): Set<T> {\n const result = new Set<T>();\n for (const item of a) {\n if (!b.has(item)) result.add(item);\n }\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/call-graph.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch, getCalleeRowsForSymbol } from '../query-support.js';\nimport type { CallGraphResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Build a call graph for a symbol: who calls it (incoming) and\n * what it calls (outgoing).\n *\n * Incoming: other symbols whose definition ranges contain a reference to this symbol.\n * Outgoing: symbols referenced within this symbol's definition range.\n */\nexport function callGraph(db: ScipDatabase, symbolPattern: string): CallGraphResult | null {\n // Find the target symbol and its definition range\n const target = findFirstSymbolMatch(db, symbolPattern);\n\n if (!target) return null;\n\n // CALLERS: symbols whose definition ranges contain a reference to our target.\n // Find chunks that reference our symbol, then find which symbol's definition encloses that chunk.\n const callerRows = db.all<{\n caller_symbol: string;\n caller_file: string;\n }>(\n `SELECT DISTINCT caller_gs.symbol AS caller_symbol, caller_d.relative_path AS caller_file\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n -- Find the enclosing symbol for where the reference appears\n JOIN defn_enclosing_ranges caller_der\n ON caller_der.document_id = ref_d.id\n AND c.start_line >= caller_der.start_line\n AND c.end_line <= caller_der.end_line\n JOIN global_symbols caller_gs ON caller_der.symbol_id = caller_gs.id\n JOIN documents caller_d ON caller_der.document_id = caller_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND caller_gs.id != ?\n ${db.symbolNoiseFor('caller_gs')}\n ${db.pathExclusionsFor('caller_d')}\n ORDER BY caller_d.relative_path\n LIMIT 50`,\n target.symbolId, target.symbolId,\n );\n\n // CALLEES: symbols referenced within our target's definition range.\n const calleeRows = getCalleeRowsForSymbol(db, target, { limit: 50 });\n\n return {\n symbol: target.symbol,\n shortName: shortenSymbol(target.symbol),\n callers: callerRows\n .filter((r) => !db.isIgnored(r.caller_file))\n .map((r) => ({\n symbol: r.caller_symbol,\n shortName: shortenSymbol(r.caller_symbol),\n file: r.caller_file,\n })),\n callees: calleeRows\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n file: r.file,\n })),\n };\n}\n"],"mappings":";;;;;;;;;AAYO,SAAS,UAAU,IAAkB,eAA+C;AAEzF,QAAM,SAAS,qBAAqB,IAAI,aAAa;AAErD,MAAI,CAAC,OAAQ,QAAO;AAIpB,QAAM,aAAa,GAAG;AAAA,IAIpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAcI,GAAG,eAAe,WAAW,CAAC;AAAA,QAC9B,GAAG,kBAAkB,UAAU,CAAC;AAAA;AAAA;AAAA,IAGpC,OAAO;AAAA,IAAU,OAAO;AAAA,EAC1B;AAGA,QAAM,aAAa,uBAAuB,IAAI,QAAQ,EAAE,OAAO,GAAG,CAAC;AAEnE,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,WAAW,cAAc,OAAO,MAAM;AAAA,IACtC,SAAS,WACN,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,WAAW,CAAC,EAC1C,IAAI,CAAC,OAAO;AAAA,MACX,QAAQ,EAAE;AAAA,MACV,WAAW,cAAc,EAAE,aAAa;AAAA,MACxC,MAAM,EAAE;AAAA,IACV,EAAE;AAAA,IACJ,SAAS,WACN,IAAI,CAAC,OAAO;AAAA,MACX,QAAQ,EAAE;AAAA,MACV,WAAW,cAAc,EAAE,MAAM;AAAA,MACjC,MAAM,EAAE;AAAA,IACV,EAAE;AAAA,EACN;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/change-surface.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { TEST_FILE_PATTERNS, testFileMatchSql } from '../query-support.js';\nimport type { ChangeSurfaceEntry, ChangeSurfaceResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Pre-change briefing for a file. For each symbol defined in the file,\n * reports external consumer count, test coverage, and risk level.\n */\nexport function changeSurface(\n db: ScipDatabase,\n filePattern: string,\n): ChangeSurfaceResult | null {\n // Find the file\n const doc = db.get<{ id: number; relative_path: string }>(\n `SELECT id, relative_path FROM documents\n WHERE relative_path LIKE ?\n ${db.pathExclusionsFor('documents')}\n LIMIT 1`,\n `%${filePattern}%`,\n );\n\n if (!doc || db.isIgnored(doc.relative_path)) return null;\n\n // Get all symbols defined in this file, excluding typeLiterals\n const syms = db.all<{\n symbol_id: number;\n symbol: string;\n start_line: number;\n end_line: number;\n }>(\n `SELECT DISTINCT gs.id AS symbol_id, gs.symbol, der.start_line, der.end_line\n FROM defn_enclosing_ranges der\n JOIN global_symbols gs ON der.symbol_id = gs.id\n WHERE der.document_id = ?\n ${db.symbolNoiseFor('gs')}\n ORDER BY der.start_line`,\n doc.id,\n );\n\n const testPatternSql = testFileMatchSql('ref_d', TEST_FILE_PATTERNS);\n\n const symbols: ChangeSurfaceEntry[] = [];\n let totalExternalConsumers = 0;\n let coveredCount = 0;\n\n for (const sym of syms) {\n // Count external consumers: mentions with role=0 from different documents\n const consumerRow = db.get<{ consumer_count: number }>(\n `SELECT COUNT(DISTINCT c.document_id) AS consumer_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND c.document_id != ?`,\n sym.symbol_id,\n doc.id,\n );\n\n const externalConsumers = consumerRow?.consumer_count ?? 0;\n\n // Find test files that reference this symbol\n const testFiles = db.all<{ relative_path: string }>(\n `SELECT DISTINCT ref_d.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND (${testPatternSql})\n ORDER BY ref_d.relative_path`,\n sym.symbol_id,\n ).map((r) => r.relative_path);\n\n const hasTests = testFiles.length > 0;\n if (hasTests) coveredCount++;\n\n // Risk level determination\n let riskLevel: 'low' | 'medium' | 'high';\n if (externalConsumers > 10 && !hasTests) {\n riskLevel = 'high';\n } else if (externalConsumers > 5 || !hasTests) {\n riskLevel = 'medium';\n } else {\n riskLevel = 'low';\n }\n\n totalExternalConsumers += externalConsumers;\n\n symbols.push({\n symbol: sym.symbol,\n shortName: shortenSymbol(sym.symbol),\n startLine: sym.start_line,\n endLine: sym.end_line,\n externalConsumers,\n testFiles,\n riskLevel,\n });\n }\n\n const testCoveragePercent =\n symbols.length > 0 ? Math.round((coveredCount / symbols.length) * 100) : 0;\n\n return {\n file: doc.relative_path,\n symbols,\n totalExternalConsumers,\n testCoveragePercent,\n };\n}\n"],"mappings":";;;;;;;;;AASO,SAAS,cACd,IACA,aAC4B;AAE5B,QAAM,MAAM,GAAG;AAAA,IACb;AAAA;AAAA,SAEK,GAAG,kBAAkB,WAAW,CAAC;AAAA;AAAA,IAEtC,IAAI,WAAW;AAAA,EACjB;AAEA,MAAI,CAAC,OAAO,GAAG,UAAU,IAAI,aAAa,EAAG,QAAO;AAGpD,QAAM,OAAO,GAAG;AAAA,IAMd;AAAA;AAAA;AAAA;AAAA,QAII,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA,IAE3B,IAAI;AAAA,EACN;AAEA,QAAM,iBAAiB,iBAAiB,SAAS,kBAAkB;AAEnE,QAAM,UAAgC,CAAC;AACvC,MAAI,yBAAyB;AAC7B,MAAI,eAAe;AAEnB,aAAW,OAAO,MAAM;AAEtB,UAAM,cAAc,GAAG;AAAA,MACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,IAAI;AAAA,MACJ,IAAI;AAAA,IACN;AAEA,UAAM,oBAAoB,aAAa,kBAAkB;AAGzD,UAAM,YAAY,GAAG;AAAA,MACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAMS,cAAc;AAAA;AAAA,MAEvB,IAAI;AAAA,IACN,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa;AAE5B,UAAM,WAAW,UAAU,SAAS;AACpC,QAAI,SAAU;AAGd,QAAI;AACJ,QAAI,oBAAoB,MAAM,CAAC,UAAU;AACvC,kBAAY;AAAA,IACd,WAAW,oBAAoB,KAAK,CAAC,UAAU;AAC7C,kBAAY;AAAA,IACd,OAAO;AACL,kBAAY;AAAA,IACd;AAEA,8BAA0B;AAE1B,YAAQ,KAAK;AAAA,MACX,QAAQ,IAAI;AAAA,MACZ,WAAW,cAAc,IAAI,MAAM;AAAA,MACnC,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,sBACJ,QAAQ,SAAS,IAAI,KAAK,MAAO,eAAe,QAAQ,SAAU,GAAG,IAAI;AAE3E,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/code.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch } from '../query-support.js';\nimport type { CodeResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Read the source code for a symbol, bounded to its definition range.\n * Language-agnostic: just reads the file and extracts the relevant lines.\n *\n * Accepts:\n * - Symbol name pattern: \"processVegaMention\"\n * - Full short name: \"src:modules:chat:processVegaMention\"\n * - File:line-line syntax: \"src/chat/service.ts:100-200\"\n */\nexport function code(\n db: ScipDatabase,\n symbolPattern: string,\n opts: { context?: number } = {},\n): CodeResult | null {\n const { context = 0 } = opts;\n\n // Handle direct file:line-line syntax (bypass symbol lookup)\n const fileLineMatch = symbolPattern.match(/^(.+\\.\\w+):(\\d+)-(\\d+)$/);\n if (fileLineMatch) {\n return readFileRange(db, fileLineMatch[1]!, parseInt(fileLineMatch[2]!, 10), parseInt(fileLineMatch[3]!, 10), context);\n }\n\n const match = findFirstSymbolMatch(db, symbolPattern);\n if (!match) return null;\n\n // Get the language from the documents table\n const doc = db.get<{ language: string | null }>(\n `SELECT language FROM documents WHERE relative_path = ?`,\n match.relativePath,\n );\n\n // Read the file\n const filePath = join(db.config.projectRoot, match.relativePath);\n let fileContent: string;\n try {\n fileContent = readFileSync(filePath, 'utf-8');\n } catch {\n return null;\n }\n\n const lines = fileContent.split('\\n');\n const startLine = Math.max(0, match.startLine - context);\n const endLine = Math.min(lines.length - 1, match.endLine + context);\n const source = lines.slice(startLine, endLine + 1).join('\\n');\n\n return {\n symbol: match.symbol,\n shortName: shortenSymbol(match.symbol),\n relativePath: match.relativePath,\n startLine: startLine + 1, // 1-indexed for display\n endLine: endLine + 1,\n language: doc?.language ?? null,\n source,\n };\n}\n\n/** Read source by file path and line range directly (no symbol lookup) */\nfunction readFileRange(\n db: ScipDatabase,\n filePath: string,\n startLine: number,\n endLine: number,\n context: number,\n): CodeResult | null {\n // Find the file in the index\n const doc = db.get<{ relative_path: string; language: string | null }>(\n `SELECT relative_path, language FROM documents WHERE relative_path LIKE ?`,\n `%${filePath}%`,\n );\n if (!doc) return null;\n\n const fullPath = join(db.config.projectRoot, doc.relative_path);\n let fileContent: string;\n try {\n fileContent = readFileSync(fullPath, 'utf-8');\n } catch {\n return null;\n }\n\n const lines = fileContent.split('\\n');\n const start = Math.max(0, startLine - 1 - context); // convert to 0-indexed\n const end = Math.min(lines.length - 1, endLine - 1 + context);\n const source = lines.slice(start, end + 1).join('\\n');\n\n return {\n symbol: `${doc.relative_path}:${startLine}-${endLine}`,\n shortName: `${doc.relative_path}:${startLine}-${endLine}`,\n relativePath: doc.relative_path,\n startLine: start + 1,\n endLine: end + 1,\n language: doc.language,\n source,\n };\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AAed,SAAS,KACd,IACA,eACA,OAA6B,CAAC,GACX;AACnB,QAAM,EAAE,UAAU,EAAE,IAAI;AAGxB,QAAM,gBAAgB,cAAc,MAAM,yBAAyB;AACnE,MAAI,eAAe;AACjB,WAAO,cAAc,IAAI,cAAc,CAAC,GAAI,SAAS,cAAc,CAAC,GAAI,EAAE,GAAG,SAAS,cAAc,CAAC,GAAI,EAAE,GAAG,OAAO;AAAA,EACvH;AAEA,QAAM,QAAQ,qBAAqB,IAAI,aAAa;AACpD,MAAI,CAAC,MAAO,QAAO;AAGnB,QAAM,MAAM,GAAG;AAAA,IACb;AAAA,IACA,MAAM;AAAA,EACR;AAGA,QAAM,WAAW,KAAK,GAAG,OAAO,aAAa,MAAM,YAAY;AAC/D,MAAI;AACJ,MAAI;AACF,kBAAc,aAAa,UAAU,OAAO;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,MAAM,IAAI;AACpC,QAAM,YAAY,KAAK,IAAI,GAAG,MAAM,YAAY,OAAO;AACvD,QAAM,UAAU,KAAK,IAAI,MAAM,SAAS,GAAG,MAAM,UAAU,OAAO;AAClE,QAAM,SAAS,MAAM,MAAM,WAAW,UAAU,CAAC,EAAE,KAAK,IAAI;AAE5D,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,WAAW,cAAc,MAAM,MAAM;AAAA,IACrC,cAAc,MAAM;AAAA,IACpB,WAAW,YAAY;AAAA;AAAA,IACvB,SAAS,UAAU;AAAA,IACnB,UAAU,KAAK,YAAY;AAAA,IAC3B;AAAA,EACF;AACF;AAGA,SAAS,cACP,IACA,UACA,WACA,SACA,SACmB;AAEnB,QAAM,MAAM,GAAG;AAAA,IACb;AAAA,IACA,IAAI,QAAQ;AAAA,EACd;AACA,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,WAAW,KAAK,GAAG,OAAO,aAAa,IAAI,aAAa;AAC9D,MAAI;AACJ,MAAI;AACF,kBAAc,aAAa,UAAU,OAAO;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,MAAM,IAAI;AACpC,QAAM,QAAQ,KAAK,IAAI,GAAG,YAAY,IAAI,OAAO;AACjD,QAAM,MAAM,KAAK,IAAI,MAAM,SAAS,GAAG,UAAU,IAAI,OAAO;AAC5D,QAAM,SAAS,MAAM,MAAM,OAAO,MAAM,CAAC,EAAE,KAAK,IAAI;AAEpD,SAAO;AAAA,IACL,QAAQ,GAAG,IAAI,aAAa,IAAI,SAAS,IAAI,OAAO;AAAA,IACpD,WAAW,GAAG,IAAI,aAAa,IAAI,SAAS,IAAI,OAAO;AAAA,IACvD,cAAc,IAAI;AAAA,IAClB,WAAW,QAAQ;AAAA,IACnB,SAAS,MAAM;AAAA,IACf,UAAU,IAAI;AAAA,IACd;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/stats.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { StatsResult } from '../types.js';\n\nexport function stats(db: ScipDatabase): StatsResult {\n const documents = db.get<{ c: number }>('SELECT COUNT(*) as c FROM documents')!.c;\n const symbols = db.get<{ c: number }>('SELECT COUNT(*) as c FROM global_symbols')!.c;\n const definitions = db.get<{ c: number }>(\n 'SELECT COUNT(*) as c FROM mentions WHERE role = 1',\n )!.c;\n const references = db.get<{ c: number }>(\n 'SELECT COUNT(*) as c FROM mentions WHERE role = 0',\n )!.c;\n\n return {\n documents,\n symbols,\n definitions,\n references,\n indexSizeBytes: db.sizeBytes(),\n lastBuilt: db.lastModified(),\n };\n}\n"],"mappings":";AAGO,SAAS,MAAM,IAA+B;AACnD,QAAM,YAAY,GAAG,IAAmB,qCAAqC,EAAG;AAChF,QAAM,UAAU,GAAG,IAAmB,0CAA0C,EAAG;AACnF,QAAM,cAAc,GAAG;AAAA,IACrB;AAAA,EACF,EAAG;AACH,QAAM,aAAa,GAAG;AAAA,IACpB;AAAA,EACF,EAAG;AAEH,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB,GAAG,UAAU;AAAA,IAC7B,WAAW,GAAG,aAAa;AAAA,EAC7B;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/diff-impact.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport type { ScipDatabase } from '../db.js';\nimport { TEST_FILE_PATTERNS, testFileMatchSql } from '../query-support.js';\nimport type { DiffImpactResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Given a git diff, compute the affected symbol set.\n * Finds all symbols defined in changed files, their fan-in,\n * the files that consume them, and test coverage gaps.\n */\nexport function diffImpact(\n db: ScipDatabase,\n opts: { base?: string } = {},\n): DiffImpactResult {\n const { base = 'HEAD' } = opts;\n\n // Get changed files from git\n let changedFileLines: string[];\n try {\n const stdout = execFileSync('git', ['diff', '--name-only', base], {\n encoding: 'utf-8',\n cwd: db.config.projectRoot,\n timeout: 10_000,\n });\n changedFileLines = stdout\n .split('\\n')\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n } catch {\n // Not in a git repo or git not available — return empty result\n return {\n changedFiles: [],\n changedSymbols: [],\n affectedConsumers: [],\n uncoveredSymbols: [],\n summary: {\n totalChangedFiles: 0,\n totalChangedSymbols: 0,\n totalAffectedFiles: 0,\n testCoveragePercent: 0,\n },\n };\n }\n\n if (changedFileLines.length === 0) {\n return {\n changedFiles: [],\n changedSymbols: [],\n affectedConsumers: [],\n uncoveredSymbols: [],\n summary: {\n totalChangedFiles: 0,\n totalChangedSymbols: 0,\n totalAffectedFiles: 0,\n testCoveragePercent: 0,\n },\n };\n }\n\n // Match changed files against the index\n const changedFiles: string[] = [];\n const changedDocIds: number[] = [];\n\n for (const file of changedFileLines) {\n const doc = db.get<{ id: number; relative_path: string }>(\n `SELECT id, relative_path FROM documents\n WHERE relative_path LIKE ?\n LIMIT 1`,\n `%${file}`,\n );\n if (doc && !db.isIgnored(doc.relative_path)) {\n changedFiles.push(doc.relative_path);\n changedDocIds.push(doc.id);\n }\n }\n\n if (changedDocIds.length === 0) {\n return {\n changedFiles: changedFileLines,\n changedSymbols: [],\n affectedConsumers: [],\n uncoveredSymbols: [],\n summary: {\n totalChangedFiles: changedFileLines.length,\n totalChangedSymbols: 0,\n totalAffectedFiles: 0,\n testCoveragePercent: 0,\n },\n };\n }\n\n // Get all symbols defined in changed files\n const docPlaceholders = changedDocIds.map(() => '?').join(',');\n const syms = db.all<{\n symbol_id: number;\n symbol: string;\n relative_path: string;\n }>(\n `SELECT DISTINCT gs.id AS symbol_id, gs.symbol, d.relative_path\n FROM defn_enclosing_ranges der\n JOIN global_symbols gs ON der.symbol_id = gs.id\n JOIN documents d ON der.document_id = d.id\n WHERE der.document_id IN (${docPlaceholders})\n ${db.symbolNoiseFor('gs')}\n ORDER BY d.relative_path`,\n ...changedDocIds,\n );\n\n // For each symbol, compute fan-in (distinct referencing documents)\n const testPatternSql = testFileMatchSql('ref_d', TEST_FILE_PATTERNS);\n const changedSymbols: DiffImpactResult['changedSymbols'] = [];\n const consumerMap = new Map<string, Set<string>>(); // file -> set of consumed symbol shortNames\n const uncoveredSymbols: DiffImpactResult['uncoveredSymbols'] = [];\n let coveredCount = 0;\n\n for (const sym of syms) {\n // Fan-in: distinct files that reference this symbol\n const fanInRow = db.get<{ fan_in: number }>(\n `SELECT COUNT(DISTINCT c.document_id) AS fan_in\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n WHERE m.symbol_id = ?\n AND m.role = 0`,\n sym.symbol_id,\n );\n\n const fanIn = fanInRow?.fan_in ?? 0;\n const shortName = shortenSymbol(sym.symbol);\n\n changedSymbols.push({\n symbol: sym.symbol,\n shortName,\n file: sym.relative_path,\n fanIn,\n });\n\n // Collect consumer files (excluding the changed files themselves)\n const consumers = db.all<{ relative_path: string }>(\n `SELECT DISTINCT ref_d.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND ref_d.relative_path NOT IN (${changedFiles.map(() => '?').join(',')})\n ${db.pathExclusionsFor('ref_d')}`,\n sym.symbol_id,\n ...changedFiles,\n );\n\n for (const consumer of consumers) {\n if (db.isIgnored(consumer.relative_path)) continue;\n if (!consumerMap.has(consumer.relative_path)) {\n consumerMap.set(consumer.relative_path, new Set());\n }\n consumerMap.get(consumer.relative_path)!.add(shortName);\n }\n\n // Check test coverage\n const hasTest = db.get<{ c: number }>(\n `SELECT COUNT(*) AS c\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND (${testPatternSql})`,\n sym.symbol_id,\n );\n\n if (hasTest && hasTest.c > 0) {\n coveredCount++;\n } else {\n uncoveredSymbols.push({\n symbol: sym.symbol,\n shortName,\n file: sym.relative_path,\n });\n }\n }\n\n // Build affected consumers list\n const affectedConsumers = [...consumerMap.entries()]\n .map(([file, symbols]) => ({ file, consumedSymbols: symbols.size }))\n .sort((a, b) => b.consumedSymbols - a.consumedSymbols);\n\n const totalSymbols = changedSymbols.length;\n const testCoveragePercent =\n totalSymbols > 0 ? Math.round((coveredCount / totalSymbols) * 100) : 0;\n\n return {\n changedFiles,\n changedSymbols,\n affectedConsumers,\n uncoveredSymbols,\n summary: {\n totalChangedFiles: changedFiles.length,\n totalChangedSymbols: totalSymbols,\n totalAffectedFiles: affectedConsumers.length,\n testCoveragePercent,\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/similar-files.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { buildFileDepGraph } from '../query-support.js';\nimport type { SimilarFileResult } from '../types.js';\n\n/**\n * Find files with similar dependency profiles.\n *\n * Two files that depend on (import from) the same set of other files\n * are structurally doing similar work. High Jaccard similarity between\n * their dependency sets = likely copy-paste variants or consolidation candidates.\n */\nexport function similarFiles(\n db: ScipDatabase,\n opts: {\n minSimilarity?: number;\n limit?: number;\n scope?: string;\n minDeps?: number;\n filePattern?: string;\n } = {},\n): SimilarFileResult[] {\n const { minSimilarity = 0.5, limit = 20, scope, minDeps = 3, filePattern } = opts;\n\n // Build dependency profile for each file\n const profiles = buildFileProfiles(db, { scope, minDeps });\n\n const results: SimilarFileResult[] = [];\n\n if (filePattern) {\n // Compare one file against all others\n const target = profiles.find((p) => p.file.includes(filePattern));\n if (!target) return [];\n\n for (const candidate of profiles) {\n if (candidate.file === target.file) continue;\n const result = compareProfiles(target, candidate, minSimilarity);\n if (result) results.push(result);\n }\n } else {\n // Pairwise comparison across all files\n for (let i = 0; i < profiles.length; i++) {\n for (let j = i + 1; j < profiles.length; j++) {\n const result = compareProfiles(profiles[i]!, profiles[j]!, minSimilarity);\n if (result) results.push(result);\n }\n if (results.length > limit * 5) break;\n }\n }\n\n results.sort((a, b) => b.similarity - a.similarity);\n return results.slice(0, limit);\n}\n\n// ── Internal ───────────────────────────────────────────────\n\ninterface FileProfile {\n file: string;\n deps: Set<string>;\n}\n\nfunction buildFileProfiles(\n db: ScipDatabase,\n opts: { scope?: string; minDeps: number },\n): FileProfile[] {\n const { scope, minDeps } = opts;\n const depMap = buildFileDepGraph(db, scope);\n const universalDeps = findUniversalDependencies(depMap);\n\n // Filter to files with enough deps\n const profiles: FileProfile[] = [];\n for (const [file, deps] of depMap) {\n if (deps.size >= minDeps) {\n profiles.push({\n file,\n deps: new Set([...deps].filter((dep) => !universalDeps.has(dep))),\n });\n }\n }\n\n return profiles;\n}\n\nfunction findUniversalDependencies(\n depMap: Map<string, Set<string>>,\n): Set<string> {\n const universalDeps = new Set<string>();\n const fileCount = depMap.size;\n if (fileCount === 0) return universalDeps;\n\n const depCounts = new Map<string, number>();\n for (const deps of depMap.values()) {\n for (const dep of deps) {\n depCounts.set(dep, (depCounts.get(dep) ?? 0) + 1);\n }\n }\n\n for (const [dep, count] of depCounts) {\n if (count / fileCount > 0.5) {\n universalDeps.add(dep);\n }\n }\n\n return universalDeps;\n}\n\nfunction compareProfiles(\n a: FileProfile,\n b: FileProfile,\n minSimilarity: number,\n): SimilarFileResult | null {\n const shared = new Set<string>();\n for (const dep of a.deps) {\n if (b.deps.has(dep)) shared.add(dep);\n }\n\n if (shared.size === 0) return null;\n\n const unionSize = new Set([...a.deps, ...b.deps]).size;\n const similarity = shared.size / unionSize;\n\n if (similarity < minSimilarity) return null;\n\n const uniqueA: string[] = [];\n for (const dep of a.deps) {\n if (!b.deps.has(dep)) uniqueA.push(dep);\n }\n const uniqueB: string[] = [];\n for (const dep of b.deps) {\n if (!a.deps.has(dep)) uniqueB.push(dep);\n }\n\n return {\n fileA: a.file,\n fileB: b.file,\n similarity,\n sharedDeps: [...shared],\n uniqueToA: uniqueA,\n uniqueToB: uniqueB,\n };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/slice.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch, getCalleeRowsForSymbol, type SymbolMatch } from '../query-support.js';\nimport type { SliceResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Reference-level program slicing: track what affects a symbol (backward)\n * or what a symbol affects (forward).\n *\n * Backward slice: \"What feeds into this?\" — symbols referenced in the same\n * function that defines the target. These are the inputs/dependencies.\n *\n * Forward slice: \"What does this feed into?\" — at each site where the target\n * is referenced, find the enclosing function, then find what that function\n * exports/defines. These are the outputs/consumers.\n *\n * Language-agnostic: works with any SCIP index.\n */\nexport function slice(\n db: ScipDatabase,\n symbolPattern: string,\n opts: { direction?: 'backward' | 'forward' } = {},\n): SliceResult | null {\n const { direction = 'backward' } = opts;\n\n const match = findFirstSymbolMatch(db, symbolPattern);\n if (!match) return null;\n\n if (direction === 'backward') {\n return backwardSlice(db, match);\n } else {\n return forwardSlice(db, match);\n }\n}\n\n\nfunction backwardSlice(db: ScipDatabase, match: SymbolMatch): SliceResult {\n // Find all symbols referenced within the definition range of the target.\n // These are what \"feeds into\" the target — the inputs.\n const callees = getCalleeRowsForSymbol(db, match);\n\n // Also find symbols whose definitions are in the same file and whose\n // ranges overlap or precede the target — local variables, parameters, etc.\n const localPredecessors = db.all<{ symbol: string; file: string }>(\n `SELECT DISTINCT gs.symbol, d.relative_path AS file\n FROM defn_enclosing_ranges der\n JOIN global_symbols gs ON der.symbol_id = gs.id\n JOIN documents d ON der.document_id = d.id\n WHERE der.document_id = ?\n AND der.end_line < ?\n AND gs.id != ?\n ${db.symbolNoiseFor('gs')}\n ORDER BY der.start_line DESC\n LIMIT 15`,\n match.documentId, match.startLine, match.symbolId,\n );\n\n const seen = new Set<string>();\n const connected: SliceResult['connectedSymbols'] = [];\n\n for (const c of callees) {\n if (seen.has(c.symbol)) continue;\n seen.add(c.symbol);\n connected.push({\n symbol: c.symbol,\n shortName: shortenSymbol(c.symbol),\n file: c.file,\n relationship: 'referenced within definition (callee)',\n });\n }\n\n for (const p of localPredecessors) {\n if (seen.has(p.symbol) || db.isIgnored(p.file)) continue;\n seen.add(p.symbol);\n connected.push({\n symbol: p.symbol,\n shortName: shortenSymbol(p.symbol),\n file: p.file,\n relationship: 'defined before target in same file (local predecessor)',\n });\n }\n\n return {\n symbol: match.symbol,\n shortName: shortenSymbol(match.symbol),\n direction: 'backward',\n connectedSymbols: connected,\n };\n}\n\nfunction forwardSlice(db: ScipDatabase, match: SymbolMatch): SliceResult {\n // Find where the target is referenced, then at each reference site,\n // find what else the enclosing function defines/exports.\n const rows = db.all<{\n enclosing_symbol: string;\n enclosing_file: string;\n output_symbol: string;\n output_file: string;\n }>(\n `SELECT DISTINCT\n enc_gs.symbol AS enclosing_symbol,\n enc_d.relative_path AS enclosing_file,\n out_gs.symbol AS output_symbol,\n out_d.relative_path AS output_file\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN documents ref_d ON ref_c.document_id = ref_d.id\n -- Find enclosing function at each reference site\n JOIN defn_enclosing_ranges enc_der\n ON enc_der.document_id = ref_d.id\n AND enc_der.start_line <= ref_c.start_line\n AND enc_der.end_line >= ref_c.end_line\n JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id\n JOIN documents enc_d ON enc_der.document_id = enc_d.id\n -- Find other symbols referenced within that enclosing function\n JOIN mentions out_m ON out_m.role = 0\n JOIN chunks out_c ON out_m.chunk_id = out_c.id\n AND out_c.document_id = enc_der.document_id\n AND out_c.start_line >= enc_der.start_line\n AND out_c.end_line <= enc_der.end_line\n JOIN global_symbols out_gs ON out_m.symbol_id = out_gs.id\n JOIN defn_enclosing_ranges out_der ON out_gs.id = out_der.symbol_id\n JOIN documents out_d ON out_der.document_id = out_d.id\n WHERE ref_m.symbol_id = ? AND ref_m.role = 0\n AND out_gs.id != ? AND out_gs.id != enc_gs.id\n AND out_d.id != ref_d.id\n ${db.symbolNoiseFor('out_gs')}\n ${db.pathExclusionsFor('out_d')}\n ORDER BY out_d.relative_path\n LIMIT 30`,\n match.symbolId, match.symbolId,\n );\n\n const seen = new Set<string>();\n const connected: SliceResult['connectedSymbols'] = [];\n\n for (const r of rows) {\n if (seen.has(r.output_symbol) || db.isIgnored(r.output_file)) continue;\n seen.add(r.output_symbol);\n connected.push({\n symbol: r.output_symbol,\n shortName: shortenSymbol(r.output_symbol),\n file: r.output_file,\n relationship: `used alongside target in ${shortenSymbol(r.enclosing_symbol)}`,\n });\n }\n\n return {\n symbol: match.symbol,\n shortName: shortenSymbol(match.symbol),\n direction: 'forward',\n connectedSymbols: connected,\n };\n}\n"],"mappings":";;;;;;;;;AAkBO,SAAS,MACd,IACA,eACA,OAA+C,CAAC,GAC5B;AACpB,QAAM,EAAE,YAAY,WAAW,IAAI;AAEnC,QAAM,QAAQ,qBAAqB,IAAI,aAAa;AACpD,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,cAAc,YAAY;AAC5B,WAAO,cAAc,IAAI,KAAK;AAAA,EAChC,OAAO;AACL,WAAO,aAAa,IAAI,KAAK;AAAA,EAC/B;AACF;AAGA,SAAS,cAAc,IAAkB,OAAiC;AAGxE,QAAM,UAAU,uBAAuB,IAAI,KAAK;AAIhD,QAAM,oBAAoB,GAAG;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOI,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA,IAG3B,MAAM;AAAA,IAAY,MAAM;AAAA,IAAW,MAAM;AAAA,EAC3C;AAEA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,YAA6C,CAAC;AAEpD,aAAW,KAAK,SAAS;AACvB,QAAI,KAAK,IAAI,EAAE,MAAM,EAAG;AACxB,SAAK,IAAI,EAAE,MAAM;AACjB,cAAU,KAAK;AAAA,MACb,QAAQ,EAAE;AAAA,MACV,WAAW,cAAc,EAAE,MAAM;AAAA,MACjC,MAAM,EAAE;AAAA,MACR,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAEA,aAAW,KAAK,mBAAmB;AACjC,QAAI,KAAK,IAAI,EAAE,MAAM,KAAK,GAAG,UAAU,EAAE,IAAI,EAAG;AAChD,SAAK,IAAI,EAAE,MAAM;AACjB,cAAU,KAAK;AAAA,MACb,QAAQ,EAAE;AAAA,MACV,WAAW,cAAc,EAAE,MAAM;AAAA,MACjC,MAAM,EAAE;AAAA,MACR,cAAc;AAAA,IAChB,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,WAAW,cAAc,MAAM,MAAM;AAAA,IACrC,WAAW;AAAA,IACX,kBAAkB;AAAA,EACpB;AACF;AAEA,SAAS,aAAa,IAAkB,OAAiC;AAGvE,QAAM,OAAO,GAAG;AAAA,IAMd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QA2BI,GAAG,eAAe,QAAQ,CAAC;AAAA,QAC3B,GAAG,kBAAkB,OAAO,CAAC;AAAA;AAAA;AAAA,IAGjC,MAAM;AAAA,IAAU,MAAM;AAAA,EACxB;AAEA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,YAA6C,CAAC;AAEpD,aAAW,KAAK,MAAM;AACpB,QAAI,KAAK,IAAI,EAAE,aAAa,KAAK,GAAG,UAAU,EAAE,WAAW,EAAG;AAC9D,SAAK,IAAI,EAAE,aAAa;AACxB,cAAU,KAAK;AAAA,MACb,QAAQ,EAAE;AAAA,MACV,WAAW,cAAc,EAAE,aAAa;AAAA,MACxC,MAAM,EAAE;AAAA,MACR,cAAc,4BAA4B,cAAc,EAAE,gBAAgB,CAAC;AAAA,IAC7E,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,WAAW,cAAc,MAAM,MAAM;AAAA,IACrC,WAAW;AAAA,IACX,kBAAkB;AAAA,EACpB;AACF;","names":[]}