@backthread/extractor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/cluster/domain-grouping.d.ts +113 -0
  4. package/dist/cluster/domain-grouping.d.ts.map +1 -0
  5. package/dist/cluster/domain-grouping.js +277 -0
  6. package/dist/cluster/domain-grouping.js.map +1 -0
  7. package/dist/cluster/grouping-gate.d.ts +54 -0
  8. package/dist/cluster/grouping-gate.d.ts.map +1 -0
  9. package/dist/cluster/grouping-gate.js +0 -0
  10. package/dist/cluster/grouping-gate.js.map +1 -0
  11. package/dist/cluster/louvain.d.ts +88 -0
  12. package/dist/cluster/louvain.d.ts.map +1 -0
  13. package/dist/cluster/louvain.js +674 -0
  14. package/dist/cluster/louvain.js.map +1 -0
  15. package/dist/cluster/overrides.d.ts +19 -0
  16. package/dist/cluster/overrides.d.ts.map +1 -0
  17. package/dist/cluster/overrides.js +32 -0
  18. package/dist/cluster/overrides.js.map +1 -0
  19. package/dist/cluster/subsystem.d.ts +45 -0
  20. package/dist/cluster/subsystem.d.ts.map +1 -0
  21. package/dist/cluster/subsystem.js +299 -0
  22. package/dist/cluster/subsystem.js.map +1 -0
  23. package/dist/cluster/workspaces.d.ts +64 -0
  24. package/dist/cluster/workspaces.d.ts.map +1 -0
  25. package/dist/cluster/workspaces.js +739 -0
  26. package/dist/cluster/workspaces.js.map +1 -0
  27. package/dist/extract.d.ts +52 -0
  28. package/dist/extract.d.ts.map +1 -0
  29. package/dist/extract.js +48 -0
  30. package/dist/extract.js.map +1 -0
  31. package/dist/framework/celery/celery.d.ts +17 -0
  32. package/dist/framework/celery/celery.d.ts.map +1 -0
  33. package/dist/framework/celery/celery.js +524 -0
  34. package/dist/framework/celery/celery.js.map +1 -0
  35. package/dist/framework/contribute-step.d.ts +38 -0
  36. package/dist/framework/contribute-step.d.ts.map +1 -0
  37. package/dist/framework/contribute-step.js +403 -0
  38. package/dist/framework/contribute-step.js.map +1 -0
  39. package/dist/framework/detect-step.d.ts +8 -0
  40. package/dist/framework/detect-step.d.ts.map +1 -0
  41. package/dist/framework/detect-step.js +54 -0
  42. package/dist/framework/detect-step.js.map +1 -0
  43. package/dist/framework/detect-util.d.ts +24 -0
  44. package/dist/framework/detect-util.d.ts.map +1 -0
  45. package/dist/framework/detect-util.js +59 -0
  46. package/dist/framework/detect-util.js.map +1 -0
  47. package/dist/framework/django/django.d.ts +22 -0
  48. package/dist/framework/django/django.d.ts.map +1 -0
  49. package/dist/framework/django/django.js +940 -0
  50. package/dist/framework/django/django.js.map +1 -0
  51. package/dist/framework/fastapi/fastapi.d.ts +27 -0
  52. package/dist/framework/fastapi/fastapi.d.ts.map +1 -0
  53. package/dist/framework/fastapi/fastapi.js +477 -0
  54. package/dist/framework/fastapi/fastapi.js.map +1 -0
  55. package/dist/framework/faststream/faststream.d.ts +19 -0
  56. package/dist/framework/faststream/faststream.d.ts.map +1 -0
  57. package/dist/framework/faststream/faststream.js +765 -0
  58. package/dist/framework/faststream/faststream.js.map +1 -0
  59. package/dist/framework/flask/flask.d.ts +17 -0
  60. package/dist/framework/flask/flask.d.ts.map +1 -0
  61. package/dist/framework/flask/flask.js +507 -0
  62. package/dist/framework/flask/flask.js.map +1 -0
  63. package/dist/framework/graphql/graphql.d.ts +17 -0
  64. package/dist/framework/graphql/graphql.d.ts.map +1 -0
  65. package/dist/framework/graphql/graphql.js +708 -0
  66. package/dist/framework/graphql/graphql.js.map +1 -0
  67. package/dist/framework/grpc/grpc.d.ts +22 -0
  68. package/dist/framework/grpc/grpc.d.ts.map +1 -0
  69. package/dist/framework/grpc/grpc.js +618 -0
  70. package/dist/framework/grpc/grpc.js.map +1 -0
  71. package/dist/framework/litestar/litestar.d.ts +17 -0
  72. package/dist/framework/litestar/litestar.d.ts.map +1 -0
  73. package/dist/framework/litestar/litestar.js +536 -0
  74. package/dist/framework/litestar/litestar.js.map +1 -0
  75. package/dist/framework/nest/nest.d.ts +19 -0
  76. package/dist/framework/nest/nest.d.ts.map +1 -0
  77. package/dist/framework/nest/nest.js +613 -0
  78. package/dist/framework/nest/nest.js.map +1 -0
  79. package/dist/framework/next/next.d.ts +22 -0
  80. package/dist/framework/next/next.d.ts.map +1 -0
  81. package/dist/framework/next/next.js +449 -0
  82. package/dist/framework/next/next.js.map +1 -0
  83. package/dist/framework/node/node.d.ts +24 -0
  84. package/dist/framework/node/node.d.ts.map +1 -0
  85. package/dist/framework/node/node.js +1009 -0
  86. package/dist/framework/node/node.js.map +1 -0
  87. package/dist/framework/orchestrator/orchestrator.d.ts +34 -0
  88. package/dist/framework/orchestrator/orchestrator.d.ts.map +1 -0
  89. package/dist/framework/orchestrator/orchestrator.js +691 -0
  90. package/dist/framework/orchestrator/orchestrator.js.map +1 -0
  91. package/dist/framework/orm/orm.d.ts +61 -0
  92. package/dist/framework/orm/orm.d.ts.map +1 -0
  93. package/dist/framework/orm/orm.js +620 -0
  94. package/dist/framework/orm/orm.js.map +1 -0
  95. package/dist/framework/python/analyze.d.ts +38 -0
  96. package/dist/framework/python/analyze.d.ts.map +1 -0
  97. package/dist/framework/python/analyze.js +98 -0
  98. package/dist/framework/python/analyze.js.map +1 -0
  99. package/dist/framework/python/py-ast.d.ts +130 -0
  100. package/dist/framework/python/py-ast.d.ts.map +1 -0
  101. package/dist/framework/python/py-ast.js +344 -0
  102. package/dist/framework/python/py-ast.js.map +1 -0
  103. package/dist/framework/python-orm/python-orm.d.ts +19 -0
  104. package/dist/framework/python-orm/python-orm.d.ts.map +1 -0
  105. package/dist/framework/python-orm/python-orm.js +516 -0
  106. package/dist/framework/python-orm/python-orm.js.map +1 -0
  107. package/dist/framework/react-native/react-native.d.ts +35 -0
  108. package/dist/framework/react-native/react-native.d.ts.map +1 -0
  109. package/dist/framework/react-native/react-native.js +1181 -0
  110. package/dist/framework/react-native/react-native.js.map +1 -0
  111. package/dist/framework/register.d.ts +2 -0
  112. package/dist/framework/register.d.ts.map +1 -0
  113. package/dist/framework/register.js +59 -0
  114. package/dist/framework/register.js.map +1 -0
  115. package/dist/framework/registry.d.ts +31 -0
  116. package/dist/framework/registry.d.ts.map +1 -0
  117. package/dist/framework/registry.js +70 -0
  118. package/dist/framework/registry.js.map +1 -0
  119. package/dist/framework/routing/expo-router.d.ts +31 -0
  120. package/dist/framework/routing/expo-router.d.ts.map +1 -0
  121. package/dist/framework/routing/expo-router.js +356 -0
  122. package/dist/framework/routing/expo-router.js.map +1 -0
  123. package/dist/framework/routing/index.d.ts +4 -0
  124. package/dist/framework/routing/index.d.ts.map +1 -0
  125. package/dist/framework/routing/index.js +16 -0
  126. package/dist/framework/routing/index.js.map +1 -0
  127. package/dist/framework/routing/next-router.d.ts +45 -0
  128. package/dist/framework/routing/next-router.d.ts.map +1 -0
  129. package/dist/framework/routing/next-router.js +378 -0
  130. package/dist/framework/routing/next-router.js.map +1 -0
  131. package/dist/framework/routing/route-tree.d.ts +73 -0
  132. package/dist/framework/routing/route-tree.d.ts.map +1 -0
  133. package/dist/framework/routing/route-tree.js +120 -0
  134. package/dist/framework/routing/route-tree.js.map +1 -0
  135. package/dist/framework/types.d.ts +230 -0
  136. package/dist/framework/types.d.ts.map +1 -0
  137. package/dist/framework/types.js +25 -0
  138. package/dist/framework/types.js.map +1 -0
  139. package/dist/graph/cross-language.d.ts +11 -0
  140. package/dist/graph/cross-language.d.ts.map +1 -0
  141. package/dist/graph/cross-language.js +123 -0
  142. package/dist/graph/cross-language.js.map +1 -0
  143. package/dist/graph/extract.d.ts +13 -0
  144. package/dist/graph/extract.d.ts.map +1 -0
  145. package/dist/graph/extract.js +79 -0
  146. package/dist/graph/extract.js.map +1 -0
  147. package/dist/graph/file-graph.d.ts +186 -0
  148. package/dist/graph/file-graph.d.ts.map +1 -0
  149. package/dist/graph/file-graph.js +427 -0
  150. package/dist/graph/file-graph.js.map +1 -0
  151. package/dist/graph/http-callsites.d.ts +14 -0
  152. package/dist/graph/http-callsites.d.ts.map +1 -0
  153. package/dist/graph/http-callsites.js +107 -0
  154. package/dist/graph/http-callsites.js.map +1 -0
  155. package/dist/graph/incremental.d.ts +122 -0
  156. package/dist/graph/incremental.d.ts.map +1 -0
  157. package/dist/graph/incremental.js +448 -0
  158. package/dist/graph/incremental.js.map +1 -0
  159. package/dist/graph/language.d.ts +31 -0
  160. package/dist/graph/language.d.ts.map +1 -0
  161. package/dist/graph/language.js +146 -0
  162. package/dist/graph/language.js.map +1 -0
  163. package/dist/graph/noise-filter.d.ts +46 -0
  164. package/dist/graph/noise-filter.d.ts.map +1 -0
  165. package/dist/graph/noise-filter.js +199 -0
  166. package/dist/graph/noise-filter.js.map +1 -0
  167. package/dist/graph/python-adapter.d.ts +36 -0
  168. package/dist/graph/python-adapter.d.ts.map +1 -0
  169. package/dist/graph/python-adapter.js +402 -0
  170. package/dist/graph/python-adapter.js.map +1 -0
  171. package/dist/graph/python-manifest.d.ts +22 -0
  172. package/dist/graph/python-manifest.d.ts.map +1 -0
  173. package/dist/graph/python-manifest.js +137 -0
  174. package/dist/graph/python-manifest.js.map +1 -0
  175. package/dist/graph/python-stdlib.d.ts +2 -0
  176. package/dist/graph/python-stdlib.d.ts.map +1 -0
  177. package/dist/graph/python-stdlib.js +53 -0
  178. package/dist/graph/python-stdlib.js.map +1 -0
  179. package/dist/graph/ts-morph-adapter.d.ts +65 -0
  180. package/dist/graph/ts-morph-adapter.d.ts.map +1 -0
  181. package/dist/graph/ts-morph-adapter.js +265 -0
  182. package/dist/graph/ts-morph-adapter.js.map +1 -0
  183. package/dist/graph/types.d.ts +37 -0
  184. package/dist/graph/types.d.ts.map +1 -0
  185. package/dist/graph/types.js +50 -0
  186. package/dist/graph/types.js.map +1 -0
  187. package/dist/index.d.ts +45 -0
  188. package/dist/index.d.ts.map +1 -0
  189. package/dist/index.js +51 -0
  190. package/dist/index.js.map +1 -0
  191. package/dist/infra/aws/aws-parse.d.ts +39 -0
  192. package/dist/infra/aws/aws-parse.d.ts.map +1 -0
  193. package/dist/infra/aws/aws-parse.js +149 -0
  194. package/dist/infra/aws/aws-parse.js.map +1 -0
  195. package/dist/infra/aws/aws.d.ts +6 -0
  196. package/dist/infra/aws/aws.d.ts.map +1 -0
  197. package/dist/infra/aws/aws.js +425 -0
  198. package/dist/infra/aws/aws.js.map +1 -0
  199. package/dist/infra/azure/azure-parse.d.ts +73 -0
  200. package/dist/infra/azure/azure-parse.d.ts.map +1 -0
  201. package/dist/infra/azure/azure-parse.js +178 -0
  202. package/dist/infra/azure/azure-parse.js.map +1 -0
  203. package/dist/infra/azure/azure.d.ts +12 -0
  204. package/dist/infra/azure/azure.d.ts.map +1 -0
  205. package/dist/infra/azure/azure.js +673 -0
  206. package/dist/infra/azure/azure.js.map +1 -0
  207. package/dist/infra/cloudflare/cloudflare.d.ts +50 -0
  208. package/dist/infra/cloudflare/cloudflare.d.ts.map +1 -0
  209. package/dist/infra/cloudflare/cloudflare.js +382 -0
  210. package/dist/infra/cloudflare/cloudflare.js.map +1 -0
  211. package/dist/infra/cloudflare/wrangler-parse.d.ts +6 -0
  212. package/dist/infra/cloudflare/wrangler-parse.d.ts.map +1 -0
  213. package/dist/infra/cloudflare/wrangler-parse.js +278 -0
  214. package/dist/infra/cloudflare/wrangler-parse.js.map +1 -0
  215. package/dist/infra/compose/compose-parse.d.ts +24 -0
  216. package/dist/infra/compose/compose-parse.d.ts.map +1 -0
  217. package/dist/infra/compose/compose-parse.js +77 -0
  218. package/dist/infra/compose/compose-parse.js.map +1 -0
  219. package/dist/infra/compose/compose.d.ts +12 -0
  220. package/dist/infra/compose/compose.d.ts.map +1 -0
  221. package/dist/infra/compose/compose.js +201 -0
  222. package/dist/infra/compose/compose.js.map +1 -0
  223. package/dist/infra/convex/convex-parse.d.ts +28 -0
  224. package/dist/infra/convex/convex-parse.d.ts.map +1 -0
  225. package/dist/infra/convex/convex-parse.js +74 -0
  226. package/dist/infra/convex/convex-parse.js.map +1 -0
  227. package/dist/infra/convex/convex.d.ts +18 -0
  228. package/dist/infra/convex/convex.d.ts.map +1 -0
  229. package/dist/infra/convex/convex.js +199 -0
  230. package/dist/infra/convex/convex.js.map +1 -0
  231. package/dist/infra/digitalocean/digitalocean.d.ts +33 -0
  232. package/dist/infra/digitalocean/digitalocean.d.ts.map +1 -0
  233. package/dist/infra/digitalocean/digitalocean.js +204 -0
  234. package/dist/infra/digitalocean/digitalocean.js.map +1 -0
  235. package/dist/infra/firebase/firebase.d.ts +43 -0
  236. package/dist/infra/firebase/firebase.d.ts.map +1 -0
  237. package/dist/infra/firebase/firebase.js +271 -0
  238. package/dist/infra/firebase/firebase.js.map +1 -0
  239. package/dist/infra/fly/fly-parse.d.ts +52 -0
  240. package/dist/infra/fly/fly-parse.d.ts.map +1 -0
  241. package/dist/infra/fly/fly-parse.js +114 -0
  242. package/dist/infra/fly/fly-parse.js.map +1 -0
  243. package/dist/infra/fly/fly.d.ts +15 -0
  244. package/dist/infra/fly/fly.d.ts.map +1 -0
  245. package/dist/infra/fly/fly.js +265 -0
  246. package/dist/infra/fly/fly.js.map +1 -0
  247. package/dist/infra/gcp/gcp-parse.d.ts +24 -0
  248. package/dist/infra/gcp/gcp-parse.d.ts.map +1 -0
  249. package/dist/infra/gcp/gcp-parse.js +123 -0
  250. package/dist/infra/gcp/gcp-parse.js.map +1 -0
  251. package/dist/infra/gcp/gcp.d.ts +10 -0
  252. package/dist/infra/gcp/gcp.d.ts.map +1 -0
  253. package/dist/infra/gcp/gcp.js +502 -0
  254. package/dist/infra/gcp/gcp.js.map +1 -0
  255. package/dist/infra/heroku/heroku-parse.d.ts +33 -0
  256. package/dist/infra/heroku/heroku-parse.d.ts.map +1 -0
  257. package/dist/infra/heroku/heroku-parse.js +63 -0
  258. package/dist/infra/heroku/heroku-parse.js.map +1 -0
  259. package/dist/infra/heroku/heroku.d.ts +24 -0
  260. package/dist/infra/heroku/heroku.d.ts.map +1 -0
  261. package/dist/infra/heroku/heroku.js +253 -0
  262. package/dist/infra/heroku/heroku.js.map +1 -0
  263. package/dist/infra/image-resolve.d.ts +56 -0
  264. package/dist/infra/image-resolve.d.ts.map +1 -0
  265. package/dist/infra/image-resolve.js +229 -0
  266. package/dist/infra/image-resolve.js.map +1 -0
  267. package/dist/infra/infra-step.d.ts +39 -0
  268. package/dist/infra/infra-step.d.ts.map +1 -0
  269. package/dist/infra/infra-step.js +57 -0
  270. package/dist/infra/infra-step.js.map +1 -0
  271. package/dist/infra/k8s/index.d.ts +78 -0
  272. package/dist/infra/k8s/index.d.ts.map +1 -0
  273. package/dist/infra/k8s/index.js +251 -0
  274. package/dist/infra/k8s/index.js.map +1 -0
  275. package/dist/infra/kamal/kamal-parse.d.ts +23 -0
  276. package/dist/infra/kamal/kamal-parse.d.ts.map +1 -0
  277. package/dist/infra/kamal/kamal-parse.js +66 -0
  278. package/dist/infra/kamal/kamal-parse.js.map +1 -0
  279. package/dist/infra/kamal/kamal.d.ts +17 -0
  280. package/dist/infra/kamal/kamal.d.ts.map +1 -0
  281. package/dist/infra/kamal/kamal.js +162 -0
  282. package/dist/infra/kamal/kamal.js.map +1 -0
  283. package/dist/infra/netlify/netlify.d.ts +33 -0
  284. package/dist/infra/netlify/netlify.d.ts.map +1 -0
  285. package/dist/infra/netlify/netlify.js +247 -0
  286. package/dist/infra/netlify/netlify.js.map +1 -0
  287. package/dist/infra/opentofu/opentofu.d.ts +3 -0
  288. package/dist/infra/opentofu/opentofu.d.ts.map +1 -0
  289. package/dist/infra/opentofu/opentofu.js +157 -0
  290. package/dist/infra/opentofu/opentofu.js.map +1 -0
  291. package/dist/infra/pulumi/pulumi-parse.d.ts +60 -0
  292. package/dist/infra/pulumi/pulumi-parse.d.ts.map +1 -0
  293. package/dist/infra/pulumi/pulumi-parse.js +155 -0
  294. package/dist/infra/pulumi/pulumi-parse.js.map +1 -0
  295. package/dist/infra/pulumi/pulumi.d.ts +23 -0
  296. package/dist/infra/pulumi/pulumi.d.ts.map +1 -0
  297. package/dist/infra/pulumi/pulumi.js +260 -0
  298. package/dist/infra/pulumi/pulumi.js.map +1 -0
  299. package/dist/infra/railway/railway-parse.d.ts +54 -0
  300. package/dist/infra/railway/railway-parse.d.ts.map +1 -0
  301. package/dist/infra/railway/railway-parse.js +200 -0
  302. package/dist/infra/railway/railway-parse.js.map +1 -0
  303. package/dist/infra/railway/railway.d.ts +19 -0
  304. package/dist/infra/railway/railway.d.ts.map +1 -0
  305. package/dist/infra/railway/railway.js +397 -0
  306. package/dist/infra/railway/railway.js.map +1 -0
  307. package/dist/infra/register.d.ts +2 -0
  308. package/dist/infra/register.d.ts.map +1 -0
  309. package/dist/infra/register.js +81 -0
  310. package/dist/infra/register.js.map +1 -0
  311. package/dist/infra/registry.d.ts +64 -0
  312. package/dist/infra/registry.d.ts.map +1 -0
  313. package/dist/infra/registry.js +205 -0
  314. package/dist/infra/registry.js.map +1 -0
  315. package/dist/infra/relevance.d.ts +16 -0
  316. package/dist/infra/relevance.d.ts.map +1 -0
  317. package/dist/infra/relevance.js +132 -0
  318. package/dist/infra/relevance.js.map +1 -0
  319. package/dist/infra/render/render-parse.d.ts +76 -0
  320. package/dist/infra/render/render-parse.d.ts.map +1 -0
  321. package/dist/infra/render/render-parse.js +155 -0
  322. package/dist/infra/render/render-parse.js.map +1 -0
  323. package/dist/infra/render/render.d.ts +11 -0
  324. package/dist/infra/render/render.d.ts.map +1 -0
  325. package/dist/infra/render/render.js +303 -0
  326. package/dist/infra/render/render.js.map +1 -0
  327. package/dist/infra/sst/sst-parse.d.ts +25 -0
  328. package/dist/infra/sst/sst-parse.d.ts.map +1 -0
  329. package/dist/infra/sst/sst-parse.js +45 -0
  330. package/dist/infra/sst/sst-parse.js.map +1 -0
  331. package/dist/infra/sst/sst.d.ts +19 -0
  332. package/dist/infra/sst/sst.d.ts.map +1 -0
  333. package/dist/infra/sst/sst.js +236 -0
  334. package/dist/infra/sst/sst.js.map +1 -0
  335. package/dist/infra/supabase/supabase-parse.d.ts +34 -0
  336. package/dist/infra/supabase/supabase-parse.d.ts.map +1 -0
  337. package/dist/infra/supabase/supabase-parse.js +113 -0
  338. package/dist/infra/supabase/supabase-parse.js.map +1 -0
  339. package/dist/infra/supabase/supabase.d.ts +15 -0
  340. package/dist/infra/supabase/supabase.d.ts.map +1 -0
  341. package/dist/infra/supabase/supabase.js +198 -0
  342. package/dist/infra/supabase/supabase.js.map +1 -0
  343. package/dist/infra/terraform/hcl-parse.d.ts +34 -0
  344. package/dist/infra/terraform/hcl-parse.d.ts.map +1 -0
  345. package/dist/infra/terraform/hcl-parse.js +203 -0
  346. package/dist/infra/terraform/hcl-parse.js.map +1 -0
  347. package/dist/infra/terraform/terraform.d.ts +14 -0
  348. package/dist/infra/terraform/terraform.d.ts.map +1 -0
  349. package/dist/infra/terraform/terraform.js +264 -0
  350. package/dist/infra/terraform/terraform.js.map +1 -0
  351. package/dist/infra/types.d.ts +151 -0
  352. package/dist/infra/types.d.ts.map +1 -0
  353. package/dist/infra/types.js +14 -0
  354. package/dist/infra/types.js.map +1 -0
  355. package/dist/infra/vercel/vercel-parse.d.ts +70 -0
  356. package/dist/infra/vercel/vercel-parse.d.ts.map +1 -0
  357. package/dist/infra/vercel/vercel-parse.js +194 -0
  358. package/dist/infra/vercel/vercel-parse.js.map +1 -0
  359. package/dist/infra/vercel/vercel.d.ts +30 -0
  360. package/dist/infra/vercel/vercel.d.ts.map +1 -0
  361. package/dist/infra/vercel/vercel.js +523 -0
  362. package/dist/infra/vercel/vercel.js.map +1 -0
  363. package/dist/infra/walk.d.ts +36 -0
  364. package/dist/infra/walk.d.ts.map +1 -0
  365. package/dist/infra/walk.js +85 -0
  366. package/dist/infra/walk.js.map +1 -0
  367. package/dist/safety.d.ts +13 -0
  368. package/dist/safety.d.ts.map +1 -0
  369. package/dist/safety.js +67 -0
  370. package/dist/safety.js.map +1 -0
  371. package/dist/types.d.ts +47 -0
  372. package/dist/types.d.ts.map +1 -0
  373. package/dist/types.js +111 -0
  374. package/dist/types.js.map +1 -0
  375. package/dist/version.d.ts +2 -0
  376. package/dist/version.d.ts.map +1 -0
  377. package/dist/version.js +35 -0
  378. package/dist/version.js.map +1 -0
  379. package/package.json +65 -0
@@ -0,0 +1,1181 @@
1
+ // (Slice 1) — the React Native / Expo FrameworkAdapter.
2
+ //
3
+ // FIRST implementation of the FrameworkAdapter contract, and `detect()` ONLY
4
+ // this slice: it decides "is this an RN/Expo app, and is it Expo or bare RN?"
5
+ // from package.json deps + config-file existence — never source content
6
+ // (never-store-source). The graph contributions (navigation graph, JS↔native
7
+ // bridge boundary, screen roles) land in . reverses the
8
+ // "connect now, group later" deferral and adds the `groupingPrior` hook:
9
+ // screens registered in one navigator (Stack/Tab/Drawer) become one feature
10
+ // subsystem (a feature-folder fallback covers route files with no navigator —
11
+ // e.g. Expo Router pages). `classificationsNeeded` stays absent (grouping is
12
+ // fully deterministic — name/path-derived, zero LLM).
13
+ //
14
+ // Detection signals (manifest + config existence only):
15
+ // * deps: `react-native` and/or `expo`
16
+ // * configs: app.json · app.config.{js,ts,cjs,mjs} · metro.config.{js,ts,cjs}
17
+ // · react-native.config.{js,ts}
18
+ // * native: ios/ + android/ dirs
19
+ //
20
+ // The scoring is a PURE function (scoreReactNative) so it unit-tests without a
21
+ // real repo dir; the adapter just gathers the fs signals and calls it — the
22
+ // same pure-builder / fs-adapter split the infra adapters use.
23
+ import { existsSync, readFileSync, statSync } from 'node:fs';
24
+ import { join, posix, relative } from 'node:path';
25
+ import { SyntaxKind, } from 'ts-morph';
26
+ import { addAllSourceFiles, buildExtractionProject, readTsconfigCompilerOptions, toId, } from '../../graph/ts-morph-adapter.js';
27
+ import { SOURCE_EXTENSIONS } from '../../graph/file-graph.js';
28
+ import { extractExpoRouterTree } from '../routing/index.js';
29
+ // Config-file name sets — existence-only checks (we never read their content).
30
+ const APP_CONFIG_NAMES = ['app.config.js', 'app.config.ts', 'app.config.cjs', 'app.config.mjs'];
31
+ const METRO_CONFIG_NAMES = ['metro.config.js', 'metro.config.ts', 'metro.config.cjs'];
32
+ const RN_CONFIG_NAMES = ['react-native.config.js', 'react-native.config.ts'];
33
+ // ---------------------------------------------------------------------------
34
+ // Signal gathering (fs) — kept thin so the scoring stays pure + testable.
35
+ /** The merged dependency map from a package.json (deps + devDeps); {} on any error. */
36
+ function readDeps(baseDir) {
37
+ try {
38
+ const pkg = JSON.parse(readFileSync(join(baseDir, 'package.json'), 'utf8'));
39
+ return { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
40
+ }
41
+ catch {
42
+ return {};
43
+ }
44
+ }
45
+ function isDir(p) {
46
+ try {
47
+ return statSync(p).isDirectory();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /** Gather the signal set for a single root dir. */
54
+ export function gatherReactNativeSignals(baseDir) {
55
+ const deps = readDeps(baseDir);
56
+ return {
57
+ hasReactNativeDep: 'react-native' in deps,
58
+ hasExpoDep: 'expo' in deps,
59
+ hasAppJson: existsSync(join(baseDir, 'app.json')),
60
+ hasAppConfig: APP_CONFIG_NAMES.some((n) => existsSync(join(baseDir, n))),
61
+ hasMetroConfig: METRO_CONFIG_NAMES.some((n) => existsSync(join(baseDir, n))),
62
+ hasRnConfig: RN_CONFIG_NAMES.some((n) => existsSync(join(baseDir, n))),
63
+ hasIosDir: isDir(join(baseDir, 'ios')),
64
+ hasAndroidDir: isDir(join(baseDir, 'android')),
65
+ };
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // Pure scoring.
69
+ /**
70
+ * Decide RN/Expo from the signal set. Returns a DetectMatch, or null if the
71
+ * signals are too weak/ambiguous to claim the stack (so the generic-TS
72
+ * fallthrough stays intact).
73
+ *
74
+ * A SUFFICIENT (match-on-its-own) signal is one of:
75
+ * * a `react-native` or `expo` dep — the authoritative signal
76
+ * * metro.config.* or react-native.config.* — RN-toolchain-specific
77
+ * * BOTH ios/ and android/ native dirs — a built RN project
78
+ *
79
+ * app.json / app.config / a single native dir are SUPPORTING signals (they
80
+ * raise confidence) but never sufficient alone — app.json in particular is too
81
+ * generic to claim a stack on its own.
82
+ *
83
+ * Variant: `expo` dep (or an `app.config.*` dynamic config) ⇒ 'expo', else
84
+ * 'bare'. Recorded in metadata + reflected in confidence.
85
+ */
86
+ export function scoreReactNative(s, rootPath = '') {
87
+ const hasDep = s.hasReactNativeDep || s.hasExpoDep;
88
+ const hasRnToolingConfig = s.hasMetroConfig || s.hasRnConfig;
89
+ const hasBothNative = s.hasIosDir && s.hasAndroidDir;
90
+ // Gate: need at least one SUFFICIENT signal. Supporting-only ⇒ no match.
91
+ if (!hasDep && !hasRnToolingConfig && !hasBothNative)
92
+ return null;
93
+ let confidence = 0;
94
+ if (s.hasReactNativeDep)
95
+ confidence += 0.5;
96
+ if (s.hasExpoDep)
97
+ confidence += 0.5;
98
+ if (s.hasMetroConfig)
99
+ confidence += 0.2;
100
+ if (s.hasRnConfig)
101
+ confidence += 0.2;
102
+ if (hasBothNative)
103
+ confidence += 0.3;
104
+ else if (s.hasIosDir || s.hasAndroidDir)
105
+ confidence += 0.1;
106
+ if (s.hasAppConfig)
107
+ confidence += 0.1;
108
+ if (s.hasAppJson)
109
+ confidence += 0.05;
110
+ confidence = Math.min(1, Number(confidence.toFixed(2)));
111
+ const variant = s.hasExpoDep || s.hasAppConfig ? 'expo' : 'bare';
112
+ return {
113
+ adapter: 'react-native',
114
+ confidence,
115
+ rootPath,
116
+ metadata: {
117
+ variant,
118
+ signals: {
119
+ reactNativeDep: s.hasReactNativeDep,
120
+ expoDep: s.hasExpoDep,
121
+ appJson: s.hasAppJson,
122
+ appConfig: s.hasAppConfig,
123
+ metroConfig: s.hasMetroConfig,
124
+ rnConfig: s.hasRnConfig,
125
+ iosDir: s.hasIosDir,
126
+ androidDir: s.hasAndroidDir,
127
+ },
128
+ },
129
+ };
130
+ }
131
+ const ROLE_PRIORITY = {
132
+ endpoint: 9, // Expo Router API route — a server boundary; the only `gateway` here
133
+ navigator: 8, // React Navigation navigator (structural container)
134
+ layout: 7, // Expo Router layout (structural container)
135
+ nativeModule: 6, // JS↔native bridge
136
+ page: 5, // Expo Router page — a concrete routed screen
137
+ screen: 4, // React Navigation screen
138
+ route: 3, // Expo Router generic routable (Expo never emits it; kept for the type)
139
+ hook: 2,
140
+ component: 1,
141
+ };
142
+ const ROLE_KIND = {
143
+ endpoint: 'gateway',
144
+ navigator: 'frontend',
145
+ layout: 'frontend',
146
+ nativeModule: 'service',
147
+ page: 'frontend',
148
+ screen: 'frontend',
149
+ route: 'frontend',
150
+ hook: 'frontend',
151
+ component: 'frontend',
152
+ };
153
+ const NAVIGATOR_FACTORIES = new Set([
154
+ 'createStackNavigator',
155
+ 'createNativeStackNavigator',
156
+ 'createBottomTabNavigator',
157
+ 'createMaterialTopTabNavigator',
158
+ 'createMaterialBottomTabNavigator',
159
+ 'createDrawerNavigator',
160
+ ]);
161
+ // `navigation.<m>('Screen')` — methods whose first string arg is a screen name.
162
+ // `navigate`/`replace` are nav-unambiguous → an unresolved string is LOGGED.
163
+ // `push`/`popTo`/`jumpTo`/`navigateDeprecated` collide with Array/other APIs
164
+ // (`arr.push('x')`), so they only emit on a REGISTRY HIT and miss silently —
165
+ // a non-screen string there is not a dropped nav target, it's not a nav call.
166
+ const NAV_LOG_METHODS = new Set(['navigate', 'replace']);
167
+ const NAV_SOFT_METHODS = new Set(['push', 'popTo', 'jumpTo', 'navigateDeprecated']);
168
+ const NATIVE_GLOBALS = new Set(['NativeModules', 'TurboModuleRegistry']);
169
+ const NATIVE_FUNCS = new Set(['requireNativeComponent', 'requireNativeModule']);
170
+ const SOURCE_EXT_SET = new Set(SOURCE_EXTENSIONS);
171
+ // Analysis is memoized on the FrameworkContext OBJECT (not repoDir): the step
172
+ // passes the SAME ctx to syntheticEdges + roleTags so they share one parse,
173
+ // while the merge walk's per-checkpoint ctx (same clone.dir path, different tree)
174
+ // gets a fresh analysis — no cross-tree staleness.
175
+ const ANALYSIS_CACHE = new WeakMap();
176
+ function roleTag(role) {
177
+ return { role, kind: ROLE_KIND[role], priority: ROLE_PRIORITY[role] };
178
+ }
179
+ // Keep the highest-priority candidate role per file (deterministic, lexical
180
+ // tiebreak) — a screen that also touches NativeModules reads as `nativeModule`.
181
+ function addRole(map, fileId, role) {
182
+ const cur = map.get(fileId);
183
+ if (cur === undefined ||
184
+ ROLE_PRIORITY[role] > ROLE_PRIORITY[cur] ||
185
+ (ROLE_PRIORITY[role] === ROLE_PRIORITY[cur] && role < cur)) {
186
+ map.set(fileId, role);
187
+ }
188
+ }
189
+ // localImportedName → resolved repo-relative file id, for a single source file.
190
+ // When ts-morph resolves the specifier we trust it; when it doesn't (a tsconfig
191
+ // path alias the in-memory Project mis-anchors), we fall back to our
192
+ // own alias resolver so `#/`-aliased screen imports still bind to real files.
193
+ function buildImportNameMap(sf, repoDir, aliasResolve) {
194
+ const map = new Map();
195
+ for (const decl of sf.getImportDeclarations()) {
196
+ const resolved = decl.getModuleSpecifierSourceFile();
197
+ let fileId;
198
+ if (resolved) {
199
+ fileId = toId(repoDir, resolved.getFilePath());
200
+ }
201
+ else {
202
+ const spec = decl.getModuleSpecifierValue();
203
+ fileId = spec ? aliasResolve(spec) : undefined;
204
+ }
205
+ if (!fileId)
206
+ continue;
207
+ const def = decl.getDefaultImport();
208
+ if (def)
209
+ map.set(def.getText(), fileId);
210
+ const ns = decl.getNamespaceImport();
211
+ if (ns)
212
+ map.set(ns.getText(), fileId);
213
+ for (const ni of decl.getNamedImports()) {
214
+ map.set((ni.getAliasNode() ?? ni.getNameNode()).getText(), fileId);
215
+ }
216
+ }
217
+ return map;
218
+ }
219
+ // Resolve a bare base path (extensionless) to a real source-file id: the path
220
+ // itself, then each source extension, then an index file, in the FIXED
221
+ // SOURCE_EXTENSIONS order so the pick is snapshot-stable.
222
+ function resolveAgainstIdSet(base, idSet) {
223
+ if (idSet.has(base))
224
+ return base;
225
+ for (const ext of SOURCE_EXTENSIONS) {
226
+ const cand = `${base}.${ext}`;
227
+ if (idSet.has(cand))
228
+ return cand;
229
+ }
230
+ for (const ext of SOURCE_EXTENSIONS) {
231
+ const cand = `${base}/index.${ext}`;
232
+ if (idSet.has(cand))
233
+ return cand;
234
+ }
235
+ return undefined;
236
+ }
237
+ // Build the alias patterns from the repo's tsconfig/jsconfig `paths`. The
238
+ // tsconfig READ (candidate search + JSONC parse: comments + trailing commas, no
239
+ // `extends` follow) is now the SHARED graph/ts-morph-adapter helper
240
+ // centralized it so this adapter and the structural extractor can't drift on
241
+ // what parses (the no-baseUrl + trailing-comma shape that previously dropped
242
+ // every alias). This adapter then keeps its OWN pattern model + id-set resolver
243
+ // (makeAliasResolver / makeSpecifierResolver), because RN must resolve arbitrary
244
+ // string specifiers — including the lazy `require('#/…')` / `import('#/…')`
245
+ // screen forms — against the file-id set, which ts-morph's static-declaration
246
+ // resolution structurally cannot do.
247
+ //
248
+ // Note: deferring to the shared reader adopts its "FIRST config declaring
249
+ // baseUrl||paths wins" rule (the config the TS compiler loads); the old local
250
+ // reader skipped a baseUrl-only config to keep hunting for one with `paths`.
251
+ // Negligible/more-correct in practice (paths + baseUrl live in the same root
252
+ // tsconfig); fixtures + real RN repos are unaffected. The '*' wildcard + leading
253
+ // './' are normalized away so resolution stays a plain join.
254
+ function readAliasPatterns(repoDir) {
255
+ const paths = readTsconfigCompilerOptions(repoDir)?.paths;
256
+ if (!paths || typeof paths !== 'object')
257
+ return [];
258
+ const out = [];
259
+ for (const [key, val] of Object.entries(paths)) {
260
+ if (!Array.isArray(val))
261
+ continue;
262
+ const wildcard = key.endsWith('/*');
263
+ const prefix = wildcard ? key.slice(0, -1) : key; // '#/*' → '#/'
264
+ const targets = val
265
+ .filter((t) => typeof t === 'string')
266
+ .map((t) => {
267
+ let s = t.replace(/^\.\//, '').replace(/^\//, '');
268
+ if (wildcard)
269
+ s = s.replace(/\*$/, '').replace(/\/$/, ''); // './src/*' → 'src'
270
+ return s;
271
+ });
272
+ if (targets.length > 0)
273
+ out.push({ prefix, wildcard, targets });
274
+ }
275
+ return out;
276
+ }
277
+ // A non-relative specifier → a real file id via the tsconfig path aliases, or
278
+ // undefined. Tries each matching pattern's targets in declared order against the
279
+ // id set (with extension/index resolution) — fully deterministic.
280
+ function makeAliasResolver(patterns, idSet) {
281
+ return (spec) => {
282
+ for (const { prefix, wildcard, targets } of patterns) {
283
+ if (wildcard) {
284
+ if (!spec.startsWith(prefix))
285
+ continue;
286
+ const rest = spec.slice(prefix.length);
287
+ for (const base of targets) {
288
+ const full = base ? `${base}/${rest}` : rest;
289
+ const r = resolveAgainstIdSet(full, idSet);
290
+ if (r)
291
+ return r;
292
+ }
293
+ }
294
+ else {
295
+ if (spec !== prefix)
296
+ continue;
297
+ for (const base of targets) {
298
+ const r = resolveAgainstIdSet(base, idSet);
299
+ if (r)
300
+ return r;
301
+ }
302
+ }
303
+ }
304
+ return undefined;
305
+ };
306
+ }
307
+ // Deterministic specifier resolver over the known source-file id set, for the
308
+ // lazy `require('…')` / `import('…')` forms: RELATIVE specifiers resolve against
309
+ // the importing file's dir; a non-relative (bare/aliased) specifier falls to the
310
+ // tsconfig alias resolver. Snapshot-stable by construction.
311
+ function makeSpecifierResolver(idSet, aliasResolve) {
312
+ return (fromFileId, spec) => {
313
+ if (!spec.startsWith('.'))
314
+ return aliasResolve(spec);
315
+ const slash = fromFileId.lastIndexOf('/');
316
+ const dir = slash >= 0 ? fromFileId.slice(0, slash) : '';
317
+ return resolveAgainstIdSet(posix.join(dir, spec), idSet);
318
+ };
319
+ }
320
+ // Structural accessors — ts-morph exposes getExpression()/getInitializer()/
321
+ // getLiteralValue() on many concrete node types but not on the base Node, and we
322
+ // reach them only after a getKind() gate, so a guarded optional-call is both safe
323
+ // and avoids a forest of per-kind casts.
324
+ function innerExpression(node) {
325
+ return node.getExpression?.();
326
+ }
327
+ function nodeInitializer(node) {
328
+ return node.getInitializer?.();
329
+ }
330
+ function literalValue(node) {
331
+ return node.getLiteralValue?.();
332
+ }
333
+ // Statically resolve a string from an expression node — a string literal, an
334
+ // `as`/parenthesis/non-null-wrapped literal (the RUNTIME value; a type cast like
335
+ // `'X' as 'Y'` is type-only), or a const / enum member / object-literal-const
336
+ // property whose value is itself a static string. Truly computed → undefined.
337
+ function staticStringValue(node, depth = 0) {
338
+ if (depth > 8)
339
+ return undefined;
340
+ switch (node.getKind()) {
341
+ case SyntaxKind.StringLiteral:
342
+ case SyntaxKind.NoSubstitutionTemplateLiteral:
343
+ return literalValue(node);
344
+ case SyntaxKind.ParenthesizedExpression:
345
+ case SyntaxKind.AsExpression:
346
+ case SyntaxKind.SatisfiesExpression:
347
+ case SyntaxKind.NonNullExpression:
348
+ case SyntaxKind.TypeAssertionExpression: {
349
+ const inner = innerExpression(node);
350
+ return inner ? staticStringValue(inner, depth + 1) : undefined;
351
+ }
352
+ case SyntaxKind.Identifier:
353
+ case SyntaxKind.PropertyAccessExpression:
354
+ return resolveConstString(node, depth);
355
+ default:
356
+ return undefined;
357
+ }
358
+ }
359
+ // Resolve an Identifier / PropertyAccess referring to a const string, enum
360
+ // member, or const object property to its string value (symbol-based; degrades
361
+ // to undefined without resolution rather than throwing).
362
+ function resolveConstString(node, depth) {
363
+ let sym;
364
+ try {
365
+ sym = node.getSymbol();
366
+ }
367
+ catch {
368
+ return undefined;
369
+ }
370
+ const decl = sym?.getValueDeclaration() ?? sym?.getDeclarations()?.[0];
371
+ if (!decl)
372
+ return undefined;
373
+ switch (decl.getKind()) {
374
+ case SyntaxKind.VariableDeclaration:
375
+ case SyntaxKind.EnumMember:
376
+ case SyntaxKind.PropertyAssignment: {
377
+ const init = nodeInitializer(decl);
378
+ return init ? staticStringValue(init, depth + 1) : undefined;
379
+ }
380
+ default:
381
+ return undefined;
382
+ }
383
+ }
384
+ // Resolve a screen-registration `name` to its runtime string value, or undefined
385
+ // when it's truly computed/dynamic. Handles a JSX string attr (`name="X"`) and a
386
+ // statically-resolvable JSX expression (`name={'X' as 'Y'}`, `name={ROUTE}`, …).
387
+ function resolveScreenName(nameAttr) {
388
+ const init = nameAttr.getInitializer();
389
+ if (!init)
390
+ return undefined;
391
+ if (init.getKind() === SyntaxKind.StringLiteral) {
392
+ return init.getLiteralValue();
393
+ }
394
+ if (init.getKind() === SyntaxKind.JsxExpression) {
395
+ const expr = init.getExpression();
396
+ return expr ? staticStringValue(expr) : undefined;
397
+ }
398
+ return undefined;
399
+ }
400
+ // Resolve a component-producing expression used in a screen registration to its
401
+ // file id. Handles BOTH eager `component={X}` and the lazy `getComponent` forms
402
+ //: an imported identifier, a namespace member, or a `require('./x')` /
403
+ // `import('./x')` (incl. a `.Named` access on either). `as`/paren/non-null
404
+ // wrappers are unwrapped. An imported symbol → its source file; a locally-defined
405
+ // component (or nested navigator) → the registering file. Anything else (an
406
+ // inline `() => <Foo/>`, a require we can't resolve install-free) → undefined,
407
+ // so the caller LOGS + skips rather than mis-register the screen.
408
+ function resolveComponentRef(expr, imports, selfFileId, resolveSpecifier, depth = 0) {
409
+ if (depth > 8)
410
+ return undefined;
411
+ switch (expr.getKind()) {
412
+ case SyntaxKind.ParenthesizedExpression:
413
+ case SyntaxKind.AsExpression:
414
+ case SyntaxKind.SatisfiesExpression:
415
+ case SyntaxKind.NonNullExpression:
416
+ case SyntaxKind.TypeAssertionExpression: {
417
+ const inner = innerExpression(expr);
418
+ return inner ? resolveComponentRef(inner, imports, selfFileId, resolveSpecifier, depth + 1) : undefined;
419
+ }
420
+ case SyntaxKind.Identifier:
421
+ return imports.get(expr.getText()) ?? selfFileId;
422
+ case SyntaxKind.PropertyAccessExpression: {
423
+ const base = expr.getExpression();
424
+ // `require('./x').Named` / `import('./x').Named` → resolve the call.
425
+ if (base.getKind() === SyntaxKind.CallExpression) {
426
+ return resolveComponentRef(base, imports, selfFileId, resolveSpecifier, depth + 1);
427
+ }
428
+ // `Ns.Component` → resolve the root identifier (imported, else self file).
429
+ if (base.getKind() === SyntaxKind.Identifier) {
430
+ return imports.get(base.getText()) ?? selfFileId;
431
+ }
432
+ return resolveComponentRef(base, imports, selfFileId, resolveSpecifier, depth + 1);
433
+ }
434
+ case SyntaxKind.CallExpression: {
435
+ const call = expr;
436
+ const callee = call.getExpression();
437
+ const isRequire = callee.getKind() === SyntaxKind.Identifier && callee.getText() === 'require';
438
+ const isImport = callee.getKind() === SyntaxKind.ImportKeyword || callee.getText() === 'import';
439
+ if (!isRequire && !isImport)
440
+ return undefined;
441
+ const arg = call.getArguments()[0];
442
+ if (!arg || arg.getKind() !== SyntaxKind.StringLiteral)
443
+ return undefined;
444
+ return resolveSpecifier(arg.getLiteralValue());
445
+ }
446
+ default:
447
+ return undefined;
448
+ }
449
+ }
450
+ // From a `getComponent={() => …}` initializer (an arrow / function expression),
451
+ // return the single expression it RETURNS — a concise arrow body, or the first
452
+ // top-level `return`'s argument in a block body. Undefined otherwise.
453
+ function getComponentReturnExpr(fn) {
454
+ const k = fn.getKind();
455
+ if (k !== SyntaxKind.ArrowFunction && k !== SyntaxKind.FunctionExpression)
456
+ return undefined;
457
+ const body = fn.getBody();
458
+ if (!body)
459
+ return undefined;
460
+ if (body.getKind() !== SyntaxKind.Block)
461
+ return body; // concise arrow body
462
+ for (const stmt of body.getStatements()) {
463
+ if (stmt.getKind() === SyntaxKind.ReturnStatement) {
464
+ return stmt.getExpression();
465
+ }
466
+ }
467
+ return undefined;
468
+ }
469
+ // The static-config screen property KEY → screen name: a plain identifier/string
470
+ // key, or a computed `[ROUTE]` key resolved to its static string. Dynamic → undefined.
471
+ function staticPropName(pa) {
472
+ const nameNode = pa.getNameNode();
473
+ if (nameNode.getKind() === SyntaxKind.ComputedPropertyName) {
474
+ const e = innerExpression(nameNode);
475
+ return e ? staticStringValue(e) : undefined;
476
+ }
477
+ if (nameNode.getKind() === SyntaxKind.StringLiteral) {
478
+ return nameNode.getLiteralValue();
479
+ }
480
+ return nameNode.getText();
481
+ }
482
+ // A static-config screen VALUE → component file id. The value is either the
483
+ // component reference directly (`Home: HomeScreen`) or a screen-config object
484
+ // (`Home: { screen: X }` / `{ component: X }` / `{ getComponent: () => … }`).
485
+ function resolveConfigScreenComponent(init, imports, selfFileId, resolveSpecifier) {
486
+ if (init.getKind() === SyntaxKind.ObjectLiteralExpression) {
487
+ const obj = init;
488
+ for (const key of ['screen', 'component', 'getComponent']) {
489
+ const p = obj.getProperty(key);
490
+ if (!p || p.getKind() !== SyntaxKind.PropertyAssignment)
491
+ continue;
492
+ const v = p.getInitializer();
493
+ if (!v)
494
+ continue;
495
+ if (key === 'getComponent') {
496
+ const ret = getComponentReturnExpr(v);
497
+ return ret ? resolveComponentRef(ret, imports, selfFileId, resolveSpecifier) : undefined;
498
+ }
499
+ return resolveComponentRef(v, imports, selfFileId, resolveSpecifier);
500
+ }
501
+ return undefined;
502
+ }
503
+ return resolveComponentRef(init, imports, selfFileId, resolveSpecifier);
504
+ }
505
+ // Collect screen registrations from one file: JSX `<X.Screen name component|
506
+ // getComponent>` plus the static `createXNavigator({ screens })` config.
507
+ // `resolveSpecifier` is the per-file (bound to this file) relative require/import
508
+ // resolver. Diagnostics accumulate into `diag` for one aggregated log.
509
+ function collectScreens(sf, imports, selfFileId, registry, screenFiles, resolveSpecifier, diag, membership) {
510
+ let isNavigator = false;
511
+ const register = (screenName, componentFile) => {
512
+ if (!registry.has(screenName))
513
+ registry.set(screenName, componentFile);
514
+ screenFiles.add(componentFile);
515
+ // record this screen's component file as a member of THIS file's
516
+ // navigator(s), so the grouping prior can group a navigator's screens into
517
+ // one feature subsystem (independent of the global first-wins registry).
518
+ membership.add(componentFile);
519
+ };
520
+ // The component file from a screen element's `component` / `getComponent` attr.
521
+ // Returns a file id (resolved), null (an attr was present but unresolvable →
522
+ // caller logs), or undefined (no component-binding attr at all).
523
+ const componentFileOf = (el) => {
524
+ const comp = el.getAttribute('component');
525
+ if (comp && comp.getKind() === SyntaxKind.JsxAttribute) {
526
+ const init = comp.getInitializer();
527
+ if (init && init.getKind() === SyntaxKind.JsxExpression) {
528
+ const e = init.getExpression();
529
+ if (e)
530
+ return resolveComponentRef(e, imports, selfFileId, resolveSpecifier) ?? null;
531
+ }
532
+ return null;
533
+ }
534
+ const getComp = el.getAttribute('getComponent');
535
+ if (getComp && getComp.getKind() === SyntaxKind.JsxAttribute) {
536
+ const init = getComp.getInitializer();
537
+ if (init && init.getKind() === SyntaxKind.JsxExpression) {
538
+ const fn = init.getExpression();
539
+ const ret = fn ? getComponentReturnExpr(fn) : undefined;
540
+ if (ret)
541
+ return resolveComponentRef(ret, imports, selfFileId, resolveSpecifier) ?? null;
542
+ }
543
+ return null;
544
+ }
545
+ return undefined;
546
+ };
547
+ const jsxEls = [
548
+ ...sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
549
+ ...sf.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
550
+ ];
551
+ for (const el of jsxEls) {
552
+ const tag = el.getTagNameNode().getText();
553
+ if (tag.endsWith('.Navigator') || tag === 'Navigator')
554
+ isNavigator = true;
555
+ if (!(tag.endsWith('.Screen') || tag === 'Screen'))
556
+ continue;
557
+ isNavigator = true;
558
+ const nameAttr = el.getAttribute('name');
559
+ if (!nameAttr || nameAttr.getKind() !== SyntaxKind.JsxAttribute)
560
+ continue;
561
+ const screenName = resolveScreenName(nameAttr);
562
+ if (screenName === undefined) {
563
+ diag.dynamicNames++;
564
+ continue;
565
+ }
566
+ const componentFile = componentFileOf(el);
567
+ if (componentFile === undefined)
568
+ continue; // no component/getComponent binding
569
+ if (componentFile === null) {
570
+ diag.unresolvedComponents.add(screenName);
571
+ continue;
572
+ }
573
+ register(screenName, componentFile);
574
+ }
575
+ // Static config API: createXNavigator({ screens: { Home: HomeScreen, … } }).
576
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
577
+ if (!NAVIGATOR_FACTORIES.has(call.getExpression().getText()))
578
+ continue;
579
+ isNavigator = true;
580
+ const arg = call.getArguments()[0];
581
+ if (!arg || arg.getKind() !== SyntaxKind.ObjectLiteralExpression)
582
+ continue;
583
+ const screensProp = arg.getProperty('screens');
584
+ if (!screensProp || screensProp.getKind() !== SyntaxKind.PropertyAssignment)
585
+ continue;
586
+ const screensInit = screensProp.getInitializer();
587
+ if (!screensInit || screensInit.getKind() !== SyntaxKind.ObjectLiteralExpression)
588
+ continue;
589
+ for (const prop of screensInit.getProperties()) {
590
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment)
591
+ continue;
592
+ const pa = prop;
593
+ const screenName = staticPropName(pa);
594
+ if (screenName === undefined) {
595
+ diag.dynamicNames++;
596
+ continue;
597
+ }
598
+ const init = pa.getInitializer();
599
+ if (!init)
600
+ continue;
601
+ const componentFile = resolveConfigScreenComponent(init, imports, selfFileId, resolveSpecifier);
602
+ if (componentFile === undefined) {
603
+ diag.unresolvedComponents.add(screenName);
604
+ continue;
605
+ }
606
+ register(screenName, componentFile);
607
+ }
608
+ }
609
+ return isNavigator;
610
+ }
611
+ // Does this file touch the JS↔native bridge (NativeModules / TurboModules /
612
+ // requireNative*)? Specific patterns only, to avoid false positives.
613
+ function usesNative(sf) {
614
+ for (const pae of sf.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)) {
615
+ const base = pae.getExpression();
616
+ if (base.getKind() === SyntaxKind.Identifier && NATIVE_GLOBALS.has(base.getText()))
617
+ return true;
618
+ }
619
+ for (const eae of sf.getDescendantsOfKind(SyntaxKind.ElementAccessExpression)) {
620
+ const base = eae.getExpression();
621
+ if (base.getKind() === SyntaxKind.Identifier && NATIVE_GLOBALS.has(base.getText()))
622
+ return true;
623
+ }
624
+ for (const vd of sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
625
+ const init = vd.getInitializer();
626
+ if (init && init.getKind() === SyntaxKind.Identifier && NATIVE_GLOBALS.has(init.getText()))
627
+ return true;
628
+ }
629
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
630
+ if (NATIVE_FUNCS.has(call.getExpression().getText()))
631
+ return true;
632
+ }
633
+ return false;
634
+ }
635
+ function hasJsx(sf) {
636
+ return (sf.getFirstDescendantByKind(SyntaxKind.JsxElement) !== undefined ||
637
+ sf.getFirstDescendantByKind(SyntaxKind.JsxSelfClosingElement) !== undefined ||
638
+ sf.getFirstDescendantByKind(SyntaxKind.JsxFragment) !== undefined);
639
+ }
640
+ // basename without extension: src/hooks/useAuth.ts → 'useAuth'.
641
+ function baseName(fileId) {
642
+ const last = fileId.split('/').pop() ?? fileId;
643
+ return last.replace(/\.[^.]+$/, '');
644
+ }
645
+ function inScope(fileId, rootPath) {
646
+ if (rootPath === '')
647
+ return true;
648
+ return fileId === rootPath || fileId.startsWith(`${rootPath}/`);
649
+ }
650
+ // ---------------------------------------------------------------------------
651
+ // feature grouping prior (the deferred "group later").
652
+ //
653
+ // PRIMARY signal: navigator membership — screens registered in one navigator
654
+ // (Stack/Tab/Drawer) belong to ONE feature subsystem, regardless of where their
655
+ // files sit on disk (the headline value: screens grouped by feature, not by
656
+ // directory). The group id derives from the NAVIGATOR FILE name (deterministic +
657
+ // meaningful — RootNavigator → 'root', AuthStack → 'auth'), NEVER an index.
658
+ //
659
+ // that primary signal OVER-COLLAPSES at real scale: production RN apps
660
+ // (Bluesky) register EVERY screen in a SINGLE `Navigation.tsx` (often via tab
661
+ // sub-navigators in one file), so navigator membership yields ONE group for the
662
+ // whole app. The fix is an adaptive per-navigator mode:
663
+ // * COHESIVE navigator (its screens share ONE feature directory) → keep them
664
+ // as one group named by the navigator (the AuthStack case, the fixtures).
665
+ // * MEGA / incoherent navigator (its screens span MANY feature directories) →
666
+ // the navigator is a routing HUB, not a feature; SPLIT its screens to their
667
+ // own feature folders (Bluesky's src/screens/<Feature>/… + src/view/screens/
668
+ // <Feature>.tsx), so a 78-screen app becomes ~dozens of legible features.
669
+ // The cohesion test is the distinct directory-feature count over the navigator's
670
+ // member screens (filename-as-feature is excluded from cohesion so same-directory
671
+ // screens read as cohesive). Logged per mode — no silent cap.
672
+ //
673
+ // FALLBACK: a feature folder for screen/route-family files in NO navigator — the
674
+ // Expo Router case (file-based routing has no navigator). The feature is the
675
+ // first meaningful directory segment under a container root (a route group
676
+ // `(tabs)` → 'tabs'). A root-level file with no determinable feature is LOGGED,
677
+ // never silently dropped (no filename fallback here — that's the SPLIT path).
678
+ //
679
+ // The contribute-step namespaces these as `<adapter>:<id>` (here
680
+ // `react-native:<id>`) and overrides the claimed modules' subsystem, beating the
681
+ // directory heuristic. Fully deterministic (name/path-derived) → no LLM, so no
682
+ // `classificationsNeeded`.
683
+ // Roles whose files are candidates for the feature-folder fallback (screens that
684
+ // somehow escaped navigator membership + the Expo Router route family).
685
+ const FALLBACK_ROLES = new Set(['screen', 'page', 'layout', 'route', 'endpoint']);
686
+ // Directory segments that are CONTAINERS, not features — stripped (repeatedly,
687
+ // outermost-first) before the first real feature segment is read. Covers the
688
+ // common RN/Expo layout roots (src · app · view(s) · screen(s) · scene(s) ·
689
+ // feature(s) · page(s) · modules), so `src/view/screens/Home.tsx` → 'Home' and
690
+ // `src/screens/Messages/ChatList.tsx` → 'Messages'.
691
+ const CONTAINER_DIRS = new Set([
692
+ 'src',
693
+ 'app',
694
+ 'view',
695
+ 'views',
696
+ 'screen',
697
+ 'screens',
698
+ 'scene',
699
+ 'scenes',
700
+ 'feature',
701
+ 'features',
702
+ 'page',
703
+ 'pages',
704
+ 'modules',
705
+ ]);
706
+ // Trailing navigator-y suffixes stripped from a navigator file's basename to get
707
+ // a clean feature name (only when something remains).
708
+ const NAV_NAME_SUFFIX = /(Navigators?|Navigation|Nav|Stack|Tabs?|Drawer|Routers?|Routes|Screens)$/;
709
+ // Basenames too generic to name a navigator group — fall back to the parent dir.
710
+ const GENERIC_NAV_BASENAMES = new Set([
711
+ 'index',
712
+ 'navigation',
713
+ 'navigator',
714
+ 'navigators',
715
+ 'routes',
716
+ 'router',
717
+ 'app',
718
+ ]);
719
+ // Deterministic slug (camelCase → kebab, drop non-alnum). Mirrors the Nest/infra
720
+ // id discipline so the group id is stable across snapshots.
721
+ function slugify(s) {
722
+ return s
723
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
724
+ .toLowerCase()
725
+ .replace(/[^a-z0-9]+/g, '-')
726
+ .replace(/^-+|-+$/g, '');
727
+ }
728
+ // Humanize a raw name into a subsystem label (split camel/kebab/snake, title-case).
729
+ function humanizeName(s) {
730
+ const words = s
731
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
732
+ .replace(/[_\-.]+/g, ' ')
733
+ .trim()
734
+ .split(/\s+/)
735
+ .filter(Boolean);
736
+ if (words.length === 0)
737
+ return s;
738
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
739
+ }
740
+ // The last directory segment of a file id (the parent dir name); '' at root.
741
+ function parentDirName(fileId) {
742
+ const segs = fileId.split('/');
743
+ return segs.length >= 2 ? segs[segs.length - 2] : '';
744
+ }
745
+ // A navigator file's feature name: its basename with a navigator-y suffix
746
+ // stripped; a generic basename (index/navigation/…) falls back to the parent dir.
747
+ function navigatorGroupName(fileId) {
748
+ let base = baseName(fileId);
749
+ if (GENERIC_NAV_BASENAMES.has(base.toLowerCase())) {
750
+ base = parentDirName(fileId) || base;
751
+ }
752
+ const stripped = base.replace(NAV_NAME_SUFFIX, '');
753
+ return stripped.length > 0 ? stripped : base;
754
+ }
755
+ // A screen/route file's feature name. Strips leading CONTAINER segments, then:
756
+ // * a remaining directory segment is the feature (a route group `(tabs)` →
757
+ // 'tabs');
758
+ // * if NOTHING remains (the file sits directly in a container, e.g.
759
+ // `src/view/screens/Home.tsx`) → `allowFilename` decides: the SPLIT path
760
+ // uses the file basename ('Home'); the Expo fallback path returns undefined
761
+ // (a root-level route — logged as ungroupable, never silently dropped).
762
+ function deriveFeature(fileId, allowFilename) {
763
+ const segs = fileId.split('/');
764
+ const file = segs.pop() ?? fileId;
765
+ let i = 0;
766
+ while (i < segs.length && CONTAINER_DIRS.has(segs[i].toLowerCase()))
767
+ i++;
768
+ const first = segs[i];
769
+ if (first) {
770
+ const stripped = first.replace(/^\((.*)\)$/, '$1'); // route group → bare name
771
+ if (stripped.length > 0)
772
+ return stripped;
773
+ }
774
+ if (!allowFilename)
775
+ return undefined;
776
+ const fn = baseName(file);
777
+ return fn.length > 0 ? fn : undefined;
778
+ }
779
+ // The directory-only feature key used for the COHESION test (no filename
780
+ // fallback): files in the same container directory share a key, so same-folder
781
+ // screens read as one cohesive feature. A file with no sub-feature directory
782
+ // keys on its container dir path, distinguishing e.g. src/view/screens from
783
+ // src/legacy/screens.
784
+ function featureDirKey(fileId) {
785
+ const dir = deriveFeature(fileId, false);
786
+ if (dir)
787
+ return dir.toLowerCase();
788
+ const slash = fileId.lastIndexOf('/');
789
+ return `dir:${slash >= 0 ? fileId.slice(0, slash) : ''}`;
790
+ }
791
+ // Build the grouping prior: per-navigator COHESIVE (one group) vs MEGA (split by
792
+ // feature folder), then a feature-folder fallback for non-navigator screen/route
793
+ // files. Deterministic by construction: sorted inputs, set-union accumulation,
794
+ // sorted output, lexical label tiebreak — so two runs are byte-identical.
795
+ function buildGroupingPrior(navMembership, roles) {
796
+ const acc = new Map();
797
+ const add = (id, label, files) => {
798
+ let g = acc.get(id);
799
+ if (!g) {
800
+ g = { label, fileIds: new Set() };
801
+ acc.set(id, g);
802
+ }
803
+ else if (label < g.label) {
804
+ g.label = label; // deterministic on the (rare) same-id/different-label case
805
+ }
806
+ for (const f of files)
807
+ g.fileIds.add(f);
808
+ };
809
+ const claimed = new Set();
810
+ const ungroupable = [];
811
+ let keptNavs = 0;
812
+ let splitNavs = 0;
813
+ // Assign one screen-ish file to its own feature folder (filename-as-feature
814
+ // when it sits directly in a container) — the SPLIT path + the mega-navigator's
815
+ // own file. A file with no determinable feature is logged, never dropped.
816
+ const splitFile = (f) => {
817
+ if (claimed.has(f))
818
+ return;
819
+ const feat = deriveFeature(f, true);
820
+ if (!feat) {
821
+ ungroupable.push(f);
822
+ return;
823
+ }
824
+ const id = slugify(feat) || 'feature';
825
+ add(id, humanizeName(feat) || id, [f]);
826
+ claimed.add(f);
827
+ };
828
+ // Per-navigator (sorted; first-claim wins so a screen in two navigators is
829
+ // deterministic). A navigator with NO statically-resolvable screens has nothing
830
+ // to group → skip (keeps the directory heuristic, not a noisy singleton).
831
+ for (const navFile of [...navMembership.keys()].sort()) {
832
+ const members = navMembership.get(navFile);
833
+ if (members.size === 0)
834
+ continue;
835
+ const memberList = [...members].sort();
836
+ const dirKeys = new Set(memberList.map(featureDirKey));
837
+ if (dirKeys.size <= 1) {
838
+ // COHESIVE — the navigator's screens are one feature; keep them together
839
+ // under the navigator name (defeats the directory heuristic — the headline
840
+ // value). Only add files not already claimed by an earlier navigator.
841
+ keptNavs++;
842
+ const name = navigatorGroupName(navFile);
843
+ const id = slugify(name) || 'navigator';
844
+ const fresh = [navFile, ...memberList].filter((f) => !claimed.has(f));
845
+ if (fresh.length > 0) {
846
+ add(id, humanizeName(name) || id, fresh);
847
+ for (const f of fresh)
848
+ claimed.add(f);
849
+ }
850
+ }
851
+ else {
852
+ // MEGA / incoherent — a routing hub spanning many features; distribute its
853
+ // screens (and the navigator file itself) to their own feature folders.
854
+ splitNavs++;
855
+ splitFile(navFile);
856
+ for (const m of memberList)
857
+ splitFile(m);
858
+ }
859
+ }
860
+ // Fallback — screen/route-family files in no navigator (Expo Router pages, or
861
+ // an orphan screen), grouped by feature folder. Root-level files with no
862
+ // determinable feature are logged (no silent cap; no filename fallback here).
863
+ for (const fileId of [...roles.keys()].sort()) {
864
+ if (!FALLBACK_ROLES.has(roles.get(fileId).role))
865
+ continue;
866
+ if (claimed.has(fileId))
867
+ continue;
868
+ const feature = deriveFeature(fileId, false);
869
+ if (!feature) {
870
+ ungroupable.push(fileId);
871
+ continue;
872
+ }
873
+ const id = slugify(feature) || 'feature';
874
+ add(id, humanizeName(feature) || id, [fileId]);
875
+ claimed.add(fileId);
876
+ }
877
+ const groups = [...acc.entries()]
878
+ .map(([id, g]) => ({ id, label: g.label, fileIds: [...g.fileIds].sort() }))
879
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
880
+ return { groups, ungroupable, keptNavs, splitNavs };
881
+ }
882
+ function analyzeExpoRouter(repoDir, rootPath) {
883
+ const tree = extractExpoRouterTree({ repoDir, rootPath });
884
+ if (!tree)
885
+ return null;
886
+ // Route roles. parentFileId + routePath carried as metadata (NOT edges).
887
+ const roles = new Map();
888
+ for (const node of tree.routes) {
889
+ roles.set(node.fileId, {
890
+ role: node.role,
891
+ kind: ROLE_KIND[node.role],
892
+ priority: ROLE_PRIORITY[node.role],
893
+ metadata: {
894
+ framework: 'react-native',
895
+ convention: tree.convention,
896
+ routePath: node.routePath,
897
+ ...(node.parentFileId !== undefined ? { parentFileId: node.parentFileId } : {}),
898
+ },
899
+ });
900
+ }
901
+ // Nav edges → `calls`. The step resolves file ids → modules, drops self-edges,
902
+ // and dedupes these AGAINST the React-Navigation edges (overlap collapses).
903
+ const edges = tree.navEdges.map((e) => ({
904
+ source: e.fromFileId,
905
+ target: e.toFileId,
906
+ kind: 'calls',
907
+ metadata: { framework: 'react-native', convention: tree.convention, relation: 'navigation' },
908
+ }));
909
+ return { edges, roles };
910
+ }
911
+ // Mirror of contribute-step.ts beats(): higher priority wins; lexical role
912
+ // tiebreak keeps the pick deterministic regardless of merge/iteration order.
913
+ function roleBeats(incoming, incumbent) {
914
+ const a = incoming.priority ?? 0;
915
+ const b = incumbent.priority ?? 0;
916
+ if (a !== b)
917
+ return a > b;
918
+ return incoming.role < incumbent.role;
919
+ }
920
+ // Fold one role map into another, collapsing a per-file collision (a file that
921
+ // matched both a React-Navigation role AND an Expo route role) by roleBeats and
922
+ // recording it for a single aggregated log — no silent pick (constraint 7).
923
+ function mergeRole(into, fileId, tag, collisions) {
924
+ const cur = into.get(fileId);
925
+ if (cur === undefined) {
926
+ into.set(fileId, tag);
927
+ return;
928
+ }
929
+ if (cur.role !== tag.role) {
930
+ let set = collisions.get(fileId);
931
+ if (!set) {
932
+ set = new Set([cur.role]);
933
+ collisions.set(fileId, set);
934
+ }
935
+ set.add(tag.role);
936
+ }
937
+ if (roleBeats(tag, cur))
938
+ into.set(fileId, tag);
939
+ }
940
+ function analyzeReactNative(ctx) {
941
+ const { repoDir, rootPath, graph } = ctx;
942
+ const project = buildExtractionProject(repoDir);
943
+ addAllSourceFiles(project, repoDir);
944
+ const sourceFiles = project.getSourceFiles();
945
+ const fileById = new Map();
946
+ for (const sf of sourceFiles)
947
+ fileById.set(toId(repoDir, sf.getFilePath()), sf);
948
+ // Pass 1 — global screen registry + navigator/screen role signals.
949
+ const registry = new Map(); // screen name → component file id
950
+ const screenFiles = new Set();
951
+ const navigatorFiles = new Set();
952
+ // navigator file id → the set of screen component files it registers
953
+ // (the navigator-membership grouping signal).
954
+ const navMembership = new Map();
955
+ const importMaps = new Map();
956
+ const idSet = new Set(fileById.keys());
957
+ const aliasResolve = makeAliasResolver(readAliasPatterns(repoDir), idSet);
958
+ const resolveSpecifier = makeSpecifierResolver(idSet, aliasResolve);
959
+ const screenDiag = { dynamicNames: 0, unresolvedComponents: new Set() };
960
+ for (const [fileId, sf] of fileById) {
961
+ if (!inScope(fileId, rootPath))
962
+ continue;
963
+ const imports = buildImportNameMap(sf, repoDir, aliasResolve);
964
+ importMaps.set(fileId, imports);
965
+ const membership = new Set();
966
+ const navigator = collectScreens(sf, imports, fileId, registry, screenFiles, (spec) => resolveSpecifier(fileId, spec), screenDiag, membership);
967
+ if (navigator) {
968
+ navigatorFiles.add(fileId);
969
+ navMembership.set(fileId, membership);
970
+ }
971
+ }
972
+ // Pass 2 — nav edges from navigate()/push() targets + native boundary files.
973
+ const navEdges = [];
974
+ const seenNav = new Set();
975
+ const nativeFiles = new Set();
976
+ let dynamicNavTargets = 0;
977
+ const unresolvedNavTargets = new Set();
978
+ for (const [fileId, sf] of fileById) {
979
+ if (!inScope(fileId, rootPath))
980
+ continue;
981
+ if (usesNative(sf))
982
+ nativeFiles.add(fileId);
983
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
984
+ const callee = call.getExpression();
985
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
986
+ continue;
987
+ const method = callee.getName();
988
+ const isLog = NAV_LOG_METHODS.has(method);
989
+ const isSoft = NAV_SOFT_METHODS.has(method);
990
+ if (!isLog && !isSoft)
991
+ continue;
992
+ const arg = call.getArguments()[0];
993
+ if (!arg)
994
+ continue;
995
+ if (arg.getKind() !== SyntaxKind.StringLiteral) {
996
+ // Dynamic target — only count it for the nav-unambiguous methods, so
997
+ // `arr.push(x)` doesn't masquerade as a dropped navigation.
998
+ if (isLog)
999
+ dynamicNavTargets++;
1000
+ continue;
1001
+ }
1002
+ const target = arg.getLiteralValue();
1003
+ const targetFile = registry.get(target);
1004
+ if (!targetFile) {
1005
+ if (isLog)
1006
+ unresolvedNavTargets.add(target);
1007
+ continue;
1008
+ }
1009
+ const key = `${fileId}→${targetFile}`;
1010
+ if (seenNav.has(key))
1011
+ continue;
1012
+ seenNav.add(key);
1013
+ navEdges.push({ source: fileId, target: targetFile, kind: 'calls', metadata: { framework: 'react-native', relation: 'navigation', screen: target } });
1014
+ }
1015
+ }
1016
+ // Pass 3 — JS↔native bridge edges: importers (from the structural graph) of a
1017
+ // native-boundary file → that file. Native code itself is off-graph (we never
1018
+ // parse ios/android source); the boundary FILE is the JS side of the bridge.
1019
+ const bridgeEdges = [];
1020
+ const seenBridge = new Set();
1021
+ if (nativeFiles.size > 0) {
1022
+ for (const e of graph.edges) {
1023
+ if (e.external)
1024
+ continue;
1025
+ if (!nativeFiles.has(e.to))
1026
+ continue;
1027
+ if (e.from === e.to)
1028
+ continue;
1029
+ const key = `${e.from}→${e.to}`;
1030
+ if (seenBridge.has(key))
1031
+ continue;
1032
+ seenBridge.add(key);
1033
+ bridgeEdges.push({ source: e.from, target: e.to, kind: 'calls', metadata: { framework: 'react-native', relation: 'native-bridge' } });
1034
+ }
1035
+ }
1036
+ // Roles — combine all signals; highest priority per file wins.
1037
+ const roleByFile = new Map();
1038
+ for (const f of navigatorFiles)
1039
+ addRole(roleByFile, f, 'navigator');
1040
+ for (const f of nativeFiles)
1041
+ addRole(roleByFile, f, 'nativeModule');
1042
+ for (const f of screenFiles)
1043
+ addRole(roleByFile, f, 'screen');
1044
+ for (const [fileId, sf] of fileById) {
1045
+ if (!inScope(fileId, rootPath))
1046
+ continue;
1047
+ if (/^use[A-Z0-9]/.test(baseName(fileId)))
1048
+ addRole(roleByFile, fileId, 'hook');
1049
+ if (hasJsx(sf))
1050
+ addRole(roleByFile, fileId, 'component');
1051
+ }
1052
+ const roles = new Map();
1053
+ for (const [fileId, role] of roleByFile)
1054
+ roles.set(fileId, roleTag(role));
1055
+ // Slice B — Expo Router routes. DETECTION-GATED: no `app/`|`src/app/` route dir
1056
+ // ⇒ analyzeExpoRouter returns null ⇒ no route roles/edges ⇒ the Slice-A output
1057
+ // (and a non-Expo repo) is byte-identical. When present, fold the route roles
1058
+ // into the SAME role map — one file can carry a React-Navigation role (Slice A)
1059
+ // AND an Expo route role (Slice B); collapse by priority, record the collision
1060
+ // — and collect the route nav edges into their own bucket (the step dedupes
1061
+ // them against the React-Navigation edges + resolves both to modules).
1062
+ const routeEdges = [];
1063
+ const roleCollisions = new Map();
1064
+ const expo = analyzeExpoRouter(repoDir, rootPath);
1065
+ if (expo) {
1066
+ for (const [fileId, tag] of expo.roles)
1067
+ mergeRole(roles, fileId, tag, roleCollisions);
1068
+ routeEdges.push(...expo.edges);
1069
+ }
1070
+ // Registration summary (a positive signal for validation) + the "no silent
1071
+ // caps" registration diagnostics: screens whose name/component couldn't be
1072
+ // statically resolved (lazy registration recovered, the rest logged).
1073
+ if (navigatorFiles.size > 0 || registry.size > 0) {
1074
+ console.log(` [react-native] registered ${registry.size} screen(s) across ${navigatorFiles.size} navigator file(s)`);
1075
+ }
1076
+ if (screenDiag.dynamicNames > 0 || screenDiag.unresolvedComponents.size > 0) {
1077
+ const parts = [];
1078
+ if (screenDiag.dynamicNames > 0)
1079
+ parts.push(`${screenDiag.dynamicNames} dynamic screen name(s)`);
1080
+ if (screenDiag.unresolvedComponents.size > 0) {
1081
+ parts.push(`${screenDiag.unresolvedComponents.size} screen(s) with an unresolvable component: ${[...screenDiag.unresolvedComponents].sort().join(', ')}`);
1082
+ }
1083
+ console.log(` [react-native] registration skipped ${parts.join(' · ')} (logged, not silently dropped)`);
1084
+ }
1085
+ // No silent caps ( constraint 7): log dropped/unresolved targets once.
1086
+ if (dynamicNavTargets > 0 || unresolvedNavTargets.size > 0) {
1087
+ const parts = [];
1088
+ if (dynamicNavTargets > 0)
1089
+ parts.push(`${dynamicNavTargets} dynamic target(s)`);
1090
+ if (unresolvedNavTargets.size > 0) {
1091
+ parts.push(`${unresolvedNavTargets.size} unregistered target(s): ${[...unresolvedNavTargets].sort().join(', ')}`);
1092
+ }
1093
+ console.log(` [react-native] skipped ${parts.join(' · ')} (logged, not silently dropped)`);
1094
+ }
1095
+ // No silent caps: a file that matched BOTH a React-Navigation role AND an Expo
1096
+ // route role had them collapsed by priority — report each (roles seen + winner),
1097
+ // never a silent pick. (The contribute-step logs the analogous per-MODULE
1098
+ // collapse after file→module resolution; this is the per-FILE collapse.)
1099
+ if (roleCollisions.size > 0) {
1100
+ const sample = [...roleCollisions.entries()]
1101
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
1102
+ .slice(0, 10)
1103
+ .map(([fileId, set]) => `${fileId} {${[...set].sort().join(', ')}} → kept '${roles.get(fileId)?.role}'`);
1104
+ console.log(` [react-native] ${roleCollisions.size} file(s) matched both a React-Navigation and an Expo Router role (collapsed by priority): ${sample.join(' · ')}` +
1105
+ (roleCollisions.size > sample.length ? ' …' : ''));
1106
+ }
1107
+ // the feature grouping prior (navigator membership + feature-folder
1108
+ // fallback). Built from the final role map (so the fallback sees every role).
1109
+ const { groups, ungroupable, keptNavs, splitNavs } = buildGroupingPrior(navMembership, roles);
1110
+ if (groups.length > 0) {
1111
+ console.log(` [react-native] grouped into ${groups.length} feature subsystem(s) from ${navMembership.size} navigator file(s) ` +
1112
+ `(${keptNavs} cohesive navigator group(s) + ${splitNavs} mega-navigator(s) split by feature folder + feature-folder fallback)`);
1113
+ }
1114
+ if (ungroupable.length > 0) {
1115
+ const sample = [...ungroupable].sort().slice(0, 10);
1116
+ console.log(` [react-native] ${ungroupable.length} screen/route file(s) had no determinable feature folder (not grouped): ${sample.join(', ')}` +
1117
+ (ungroupable.length > sample.length ? ' …' : ''));
1118
+ }
1119
+ // Deterministic ordering — module-id resolution + dedupe downstream rely on a
1120
+ // stable input order; sort by endpoints.
1121
+ const byEndpoints = (a, b) => a.source < b.source ? -1 : a.source > b.source ? 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0;
1122
+ navEdges.sort(byEndpoints);
1123
+ bridgeEdges.sort(byEndpoints);
1124
+ routeEdges.sort(byEndpoints);
1125
+ return { navEdges, bridgeEdges, routeEdges, roles, groups };
1126
+ }
1127
+ function getAnalysis(ctx) {
1128
+ let a = ANALYSIS_CACHE.get(ctx);
1129
+ if (!a) {
1130
+ a = analyzeReactNative(ctx);
1131
+ ANALYSIS_CACHE.set(ctx, a);
1132
+ }
1133
+ return a;
1134
+ }
1135
+ // ---------------------------------------------------------------------------
1136
+ // The adapter.
1137
+ export const reactNativeAdapter = {
1138
+ name: 'react-native',
1139
+ async detect(ctx) {
1140
+ // Thin single-root pass: scan the workspace package when given, else the
1141
+ // repo root (per-package fan-out is ). rootPath is the repo-relative
1142
+ // posix path of that base ('' for the repo root).
1143
+ const base = ctx.packageDir ?? ctx.repoDir;
1144
+ const rootPath = ctx.packageDir
1145
+ ? relative(ctx.repoDir, ctx.packageDir).split('\\').join('/')
1146
+ : '';
1147
+ return scoreReactNative(gatherReactNativeSignals(base), rootPath);
1148
+ },
1149
+ // the feature grouping prior: navigator membership (primary) +
1150
+ // feature-folder fallback. The contribute-step namespaces each group
1151
+ // `react-native:<id>` and overrides the claimed modules' subsystem, beating the
1152
+ // directory heuristic (screens grouped by feature, not by folder). Deterministic
1153
+ // (name/path-derived) → no classificationsNeeded (the deferred-LLM channel stays
1154
+ // empty, preserving the zero-LLM property).
1155
+ async groupingPrior(ctx) {
1156
+ return { groups: getAnalysis(ctx).groups };
1157
+ },
1158
+ // the navigation graph + JS↔native boundary (file-id space; the
1159
+ // contribution step resolves to modules). kind 'calls' only.
1160
+ async syntheticEdges(ctx) {
1161
+ const a = getAnalysis(ctx);
1162
+ // React-Navigation nav + JS↔native bridge (Slice A) + Expo Router route nav
1163
+ // (Slice B). All `calls`, file-id endpoints; the step dedupes the union.
1164
+ return [...a.navEdges, ...a.bridgeEdges, ...a.routeEdges];
1165
+ },
1166
+ async roleTags(ctx) {
1167
+ return getAnalysis(ctx).roles;
1168
+ },
1169
+ // The hooks READ SOURCE — React-Navigation navigator config + navigate() calls
1170
+ // + NativeModules usage (Slice A), AND the Expo Router `app/` route files +
1171
+ // their <Link href>/router.push() nav targets (Slice B) — so declare the
1172
+ // source paths the diff-driven hosted walk must treat as framework-relevant.
1173
+ // Every Expo route file is a source file (.ts/.tsx/.js/.jsx, incl. `+api.ts`),
1174
+ // so the source-extension test already covers them. Never-store-source holds:
1175
+ // read server-side, persist only derived edges/roles.
1176
+ scansSourcePath(path) {
1177
+ const ext = path.split('.').pop();
1178
+ return ext !== undefined && SOURCE_EXT_SET.has(ext);
1179
+ },
1180
+ };
1181
+ //# sourceMappingURL=react-native.js.map