@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,1009 @@
1
+ // (Slice 2) — the Node backend FrameworkAdapter.
2
+ // the request-spine contribution hooks (roleTags + syntheticEdges).
3
+ //
4
+ // DETECTION: decides "is this a Node HTTP-server backend, and on which
5
+ // framework?" from package.json deps — never source content (never-store-source).
6
+ // These libs ship no canonical config file, so detection is dep-only.
7
+ //
8
+ // CONTRIBUTION (, founder-scoped 2026-06-26 — LIGHTWEIGHT): renders the
9
+ // request spine as ROLE TAGS on EXISTING files + request-flow `calls` EDGES —
10
+ // NO synthetic route nodes. The route's method+path is METADATA on the handler
11
+ // module, never a standalone node. We statically read source via ts-morph
12
+ // (install-free; never-store-source — read server-side, persist only the derived
13
+ // edges/roles) and contribute in the graph FILE-ID space; the generic
14
+ // contribute-step resolves to MODULE ids, drops self-edges, dedupes + 8-verb-
15
+ // validates.
16
+ //
17
+ // GROUPING (the deferred grouping, now that 's shared
18
+ // mechanism exists): a `groupingPrior` groups the request spine by ROUTE PREFIX /
19
+ // ROUTER MOUNT. A router mounted at `/admin` (`app.use('/admin', adminRouter)`,
20
+ // Hono `app.route('/api', subApp)`) and the routes it serves form one subsystem;
21
+ // direct routes group by their path's distinguishing prefix segment (a leading
22
+ // `api`/version noise segment is skipped, so `/api/users` + `/api/orders` →
23
+ // `users` / `orders`). Each prefix → one deterministic `node:<slug>` subsystem
24
+ // (slug derived from the prefix, NEVER a Louvain/array index — the snapshot-
25
+ // stability invariant), over the controller + route-handler + one-hop service-
26
+ // tail files under it. The entrypoint (server root) is NEVER swept into a route
27
+ // group (it spans every prefix). Dynamic/unresolvable mounts + ungroupable routes
28
+ // DEGRADE + LOG (no silent caps). classificationsNeeded stays absent — grouping is
29
+ // fully deterministic, no LLM touch. The contribute-step (, untouched here)
30
+ // resolves the FILE-id groups to MODULE ids and overrides each claimed module's
31
+ // subsystem, AUTHORITATIVE over directory + workspace-package grouping.
32
+ //
33
+ // Detection signals (manifest only):
34
+ // * dep: one (or more) of `express` · `fastify` · `koa` · `hono`
35
+ //
36
+ // The matched framework(s) are recorded in metadata. scoreNode is PURE.
37
+ import { posix } from 'node:path';
38
+ import { SyntaxKind, } from 'ts-morph';
39
+ import { addAllSourceFiles, buildExtractionProject, toId, } from '../../graph/ts-morph-adapter.js';
40
+ import { SOURCE_EXTENSIONS } from '../../graph/file-graph.js';
41
+ import { clampConfidence, readDeps, resolveBase } from '../detect-util.js';
42
+ // Priority order = the deterministic primary pick when a repo carries more than
43
+ // one (rare). Not a quality ranking — just a stable tiebreak so `variant` is
44
+ // reproducible. Express first (most common), then fastify/koa/hono.
45
+ const NODE_FRAMEWORKS = ['express', 'fastify', 'koa', 'hono'];
46
+ /** Gather the signal set for a single root dir (fs only). */
47
+ export function gatherNodeSignals(baseDir) {
48
+ const deps = readDeps(baseDir);
49
+ return { frameworks: NODE_FRAMEWORKS.filter((f) => f in deps) };
50
+ }
51
+ /**
52
+ * Decide Node-backend from the signal set. Returns a DetectMatch, or null when
53
+ * no server-framework dep is present (generic-TS fallthrough intact).
54
+ *
55
+ * Each server-framework dep is SUFFICIENT (authoritative). A single dep scores
56
+ * the base confidence; co-present frameworks (a repo mounting more than one)
57
+ * nudge it up — there's no config file to corroborate, so deps are the whole
58
+ * rubric. `variant` is the priority-primary framework; `frameworks` lists all.
59
+ */
60
+ export function scoreNode(s, rootPath = '') {
61
+ if (s.frameworks.length === 0)
62
+ return null;
63
+ const confidence = clampConfidence(0.6 + 0.1 * (s.frameworks.length - 1));
64
+ return {
65
+ adapter: 'node',
66
+ confidence,
67
+ rootPath,
68
+ metadata: {
69
+ variant: s.frameworks[0],
70
+ frameworks: s.frameworks,
71
+ },
72
+ };
73
+ }
74
+ const ROLE_PRIORITY = {
75
+ entrypoint: 5, // the server root — unique, structural
76
+ 'route-handler': 4, // handles a concrete request
77
+ controller: 3, // a router/grouping container
78
+ middleware: 2, // cross-cutting request step
79
+ service: 1, // leaf business logic
80
+ };
81
+ const ROLE_KIND = {
82
+ entrypoint: 'gateway',
83
+ 'route-handler': 'gateway',
84
+ controller: 'gateway',
85
+ middleware: 'gateway',
86
+ service: 'service',
87
+ };
88
+ // HTTP verb methods Express / @koa/router / Hono / Fastify all expose as
89
+ // `app.METHOD(path, …)`. `all` matches every verb. `app.set`/`app.get('cfg')`
90
+ // (Express config) are disambiguated by the ≥2-arg + string-path + callable-arg
91
+ // rule — a 1-arg `app.get('title')` config read is never a route.
92
+ const HTTP_METHODS = new Set([
93
+ 'get',
94
+ 'post',
95
+ 'put',
96
+ 'patch',
97
+ 'delete',
98
+ 'options',
99
+ 'head',
100
+ 'all',
101
+ ]);
102
+ // Fastify route-options object hook keys → middleware (the non-terminal request
103
+ // steps). `handler` (terminal) is pulled separately.
104
+ const FASTIFY_HOOK_KEYS = ['preHandler', 'onRequest', 'preValidation', 'preParsing', 'onError'];
105
+ // App-factory identifiers / classes whose construction marks the server entry.
106
+ const FACTORY_CALL_IDENTS = new Set(['express', 'fastify', 'Fastify']);
107
+ const FACTORY_NEW_IDENTS = new Set(['Koa', 'Hono', 'Fastify']);
108
+ // Router-class constructors (`new Router()` — Koa's @koa/router / express.Router
109
+ // used as a class) → a controller (router-grouping module), not an entrypoint.
110
+ const CONTROLLER_NEW_IDENTS = new Set(['Router']);
111
+ const SOURCE_EXT_SET = new Set(SOURCE_EXTENSIONS);
112
+ // Memoized on the FrameworkContext OBJECT (not repoDir) so syntheticEdges +
113
+ // roleTags share one parse, while the merge walk's per-checkpoint ctx (same
114
+ // clone.dir, different tree) gets a fresh analysis — no cross-tree staleness.
115
+ const ANALYSIS_CACHE = new WeakMap();
116
+ // ---------------------------------------------------------------------------
117
+ // Static resolution helpers (install-free, deterministic).
118
+ // localImportedName → resolved repo-relative file id, for one source file. Only
119
+ // INTERNAL imports resolve (getModuleSpecifierSourceFile is null for a bare
120
+ // external specifier install-free) — so a name absent from this map is either a
121
+ // local definition or an external import, which the callers disambiguate.
122
+ function buildImportNameMap(sf, repoDir) {
123
+ const map = new Map();
124
+ for (const decl of sf.getImportDeclarations()) {
125
+ const resolved = decl.getModuleSpecifierSourceFile();
126
+ if (!resolved)
127
+ continue;
128
+ const fileId = toId(repoDir, resolved.getFilePath());
129
+ const def = decl.getDefaultImport();
130
+ if (def)
131
+ map.set(def.getText(), fileId);
132
+ const ns = decl.getNamespaceImport();
133
+ if (ns)
134
+ map.set(ns.getText(), fileId);
135
+ for (const ni of decl.getNamedImports()) {
136
+ map.set((ni.getAliasNode() ?? ni.getNameNode()).getText(), fileId);
137
+ }
138
+ }
139
+ return map;
140
+ }
141
+ // Deterministic relative-specifier resolver over the known source-file id set —
142
+ // `require('./x')` / `import('./x')` → matching file id, or undefined. Tries the
143
+ // path, then each source extension, then an index file, in the FIXED
144
+ // SOURCE_EXTENSIONS order so the pick is snapshot-stable.
145
+ function makeSpecifierResolver(idSet) {
146
+ return (fromFileId, spec) => {
147
+ if (!spec.startsWith('.'))
148
+ return undefined;
149
+ const slash = fromFileId.lastIndexOf('/');
150
+ const dir = slash >= 0 ? fromFileId.slice(0, slash) : '';
151
+ const base = posix.join(dir, spec);
152
+ if (idSet.has(base))
153
+ return base;
154
+ for (const ext of SOURCE_EXTENSIONS) {
155
+ const cand = `${base}.${ext}`;
156
+ if (idSet.has(cand))
157
+ return cand;
158
+ }
159
+ for (const ext of SOURCE_EXTENSIONS) {
160
+ const cand = `${base}/index.${ext}`;
161
+ if (idSet.has(cand))
162
+ return cand;
163
+ }
164
+ return undefined;
165
+ };
166
+ }
167
+ // ts-morph exposes getExpression()/getLiteralValue() on many concrete node types
168
+ // but not on the base Node; we reach them only after a getKind() gate, so a
169
+ // guarded optional-call is both safe and avoids a forest of per-kind casts.
170
+ function innerExpression(node) {
171
+ return node.getExpression?.();
172
+ }
173
+ function literalValue(node) {
174
+ return node.getLiteralValue?.();
175
+ }
176
+ // Statically resolve a string from an expression node — a string literal or an
177
+ // `as`/paren/non-null-wrapped literal (the RUNTIME value). Truly computed (a
178
+ // template with substitutions, a binary concat, a variable) → undefined.
179
+ function staticString(node, depth = 0) {
180
+ if (depth > 8)
181
+ return undefined;
182
+ switch (node.getKind()) {
183
+ case SyntaxKind.StringLiteral:
184
+ case SyntaxKind.NoSubstitutionTemplateLiteral:
185
+ return literalValue(node);
186
+ case SyntaxKind.ParenthesizedExpression:
187
+ case SyntaxKind.AsExpression:
188
+ case SyntaxKind.SatisfiesExpression:
189
+ case SyntaxKind.NonNullExpression:
190
+ case SyntaxKind.TypeAssertionExpression: {
191
+ const inner = innerExpression(node);
192
+ return inner ? staticString(inner, depth + 1) : undefined;
193
+ }
194
+ default:
195
+ return undefined;
196
+ }
197
+ }
198
+ // The leftmost identifier of a property-access chain (`a.b.c` → `a`). Undefined
199
+ // when the chain doesn't root in a plain identifier.
200
+ function rootIdentifier(node, depth = 0) {
201
+ if (depth > 16)
202
+ return undefined;
203
+ if (node.getKind() === SyntaxKind.Identifier)
204
+ return node.getText();
205
+ if (node.getKind() === SyntaxKind.PropertyAccessExpression) {
206
+ return rootIdentifier(node.getExpression(), depth + 1);
207
+ }
208
+ return undefined;
209
+ }
210
+ // Is this expression a callable suitable for a handler/middleware slot? (Used to
211
+ // decide whether a route arg participates in the handler chain at all.)
212
+ function isCallableArg(node) {
213
+ switch (node.getKind()) {
214
+ case SyntaxKind.ArrowFunction:
215
+ case SyntaxKind.FunctionExpression:
216
+ case SyntaxKind.Identifier:
217
+ case SyntaxKind.PropertyAccessExpression:
218
+ case SyntaxKind.CallExpression:
219
+ return true;
220
+ default:
221
+ return false;
222
+ }
223
+ }
224
+ // Resolve a handler/middleware expression to its file id.
225
+ // * inline arrow/function → the registering file (self)
226
+ // * imported identifier → the imported file; a bare local id → self
227
+ // * `ctrl.method` / `barrel.fn` → the root identifier's file (imported) or self
228
+ // * `require('./x')` / `import('./x')` → the resolved specifier
229
+ // * a factory call `mw()` → INTERNAL import → its file; external/local → null
230
+ // Returns a file id, or null when it can't be statically resolved (caller LOGS).
231
+ function resolveHandlerFile(expr, imports, selfFileId, resolveSpecifier, depth = 0) {
232
+ if (depth > 8)
233
+ return null;
234
+ switch (expr.getKind()) {
235
+ case SyntaxKind.ParenthesizedExpression:
236
+ case SyntaxKind.AsExpression:
237
+ case SyntaxKind.SatisfiesExpression:
238
+ case SyntaxKind.NonNullExpression:
239
+ case SyntaxKind.TypeAssertionExpression: {
240
+ const inner = innerExpression(expr);
241
+ return inner ? resolveHandlerFile(inner, imports, selfFileId, resolveSpecifier, depth + 1) : null;
242
+ }
243
+ case SyntaxKind.ArrowFunction:
244
+ case SyntaxKind.FunctionExpression:
245
+ return selfFileId; // inline handler → defined in the registering file
246
+ case SyntaxKind.Identifier:
247
+ // imported → its file; an unmapped bare id is a local definition → self.
248
+ return imports.get(expr.getText()) ?? selfFileId;
249
+ case SyntaxKind.PropertyAccessExpression: {
250
+ const base = expr.getExpression();
251
+ if (base.getKind() === SyntaxKind.CallExpression) {
252
+ // `require('./x').named` / `import('./x').named`
253
+ return resolveHandlerFile(base, imports, selfFileId, resolveSpecifier, depth + 1);
254
+ }
255
+ const root = rootIdentifier(expr);
256
+ return root ? (imports.get(root) ?? selfFileId) : null;
257
+ }
258
+ case SyntaxKind.CallExpression: {
259
+ const call = expr;
260
+ const callee = call.getExpression();
261
+ const isRequire = callee.getKind() === SyntaxKind.Identifier && callee.getText() === 'require';
262
+ const isImport = callee.getKind() === SyntaxKind.ImportKeyword || callee.getText() === 'import';
263
+ if (isRequire || isImport) {
264
+ const arg = call.getArguments()[0];
265
+ if (!arg || arg.getKind() !== SyntaxKind.StringLiteral)
266
+ return null;
267
+ return resolveSpecifier(arg.getLiteralValue()) ?? null;
268
+ }
269
+ // A factory call like `cors()` / `makeAuth()`: only an INTERNAL imported
270
+ // factory resolves (→ its file); an external (`cors`) or local factory is
271
+ // not in the import map → null (logged, not silently attributed to self).
272
+ const root = rootIdentifier(callee);
273
+ return root ? (imports.get(root) ?? null) : null;
274
+ }
275
+ default:
276
+ return null;
277
+ }
278
+ }
279
+ // ---------------------------------------------------------------------------
280
+ // Pure-ish per-file scanning. Endpoints are repo-relative posix FILE ids.
281
+ function inScope(fileId, rootPath) {
282
+ if (rootPath === '')
283
+ return true;
284
+ return fileId === rootPath || fileId.startsWith(`${rootPath}/`);
285
+ }
286
+ // Keep the highest-priority candidate role per file (deterministic, lexical
287
+ // tiebreak) — mirrors RN's addRole + the contribute-step's module-level collapse.
288
+ function addRole(acc, fileId, role) {
289
+ const cur = acc.roleByFile.get(fileId);
290
+ if (cur === undefined ||
291
+ ROLE_PRIORITY[role] > ROLE_PRIORITY[cur] ||
292
+ (ROLE_PRIORITY[role] === ROLE_PRIORITY[cur] && role < cur)) {
293
+ acc.roleByFile.set(fileId, role);
294
+ }
295
+ }
296
+ function addRouteLabel(acc, fileId, label) {
297
+ let list = acc.routesByFile.get(fileId);
298
+ if (!list) {
299
+ list = [];
300
+ acc.routesByFile.set(fileId, list);
301
+ }
302
+ if (!list.some((r) => r.method === label.method && r.path === label.path))
303
+ list.push(label);
304
+ }
305
+ function addEdge(acc, from, to, metadata) {
306
+ const key = `${from}→${to}`;
307
+ if (!acc.edges.has(key))
308
+ acc.edges.set(key, { source: from, target: to, kind: 'calls', metadata });
309
+ }
310
+ // Resolve the route path(s): a static string, or an array of static strings
311
+ // (Express `app.get(['/a','/b'], …)`). Empty array ⇒ dynamic.
312
+ function resolvePaths(arg) {
313
+ const s = staticString(arg);
314
+ if (s !== undefined)
315
+ return [s];
316
+ if (arg.getKind() === SyntaxKind.ArrayLiteralExpression) {
317
+ const out = [];
318
+ for (const el of arg.getElements()) {
319
+ const v = staticString(el);
320
+ if (v === undefined)
321
+ return null; // any dynamic element ⇒ degrade the whole reg
322
+ out.push(v);
323
+ }
324
+ return out.length ? out : null;
325
+ }
326
+ return null;
327
+ }
328
+ // Process one route registration's handler chain (the args AFTER the path):
329
+ // resolve the terminal handler + the middleware chain, emit edges from the
330
+ // registering file, and tag roles + route labels. Handles positional handlers,
331
+ // handler arrays, and a Fastify options object (`{ handler, preHandler, … }`).
332
+ function processHandlerChain(args, selfFileId, labels, imports, resolveSpecifier, acc) {
333
+ const callables = []; // positional handler/middleware expressions, in order
334
+ let optsHandler; // a Fastify opts-object `handler`
335
+ const optsMiddleware = []; // Fastify opts-object hooks
336
+ for (const arg of args) {
337
+ if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) {
338
+ const obj = arg;
339
+ const h = obj.getProperty('handler');
340
+ if (h && h.getKind() === SyntaxKind.PropertyAssignment) {
341
+ const v = h.getInitializer();
342
+ if (v)
343
+ optsHandler = v;
344
+ }
345
+ for (const key of FASTIFY_HOOK_KEYS) {
346
+ const p = obj.getProperty(key);
347
+ if (!p || p.getKind() !== SyntaxKind.PropertyAssignment)
348
+ continue;
349
+ const v = p.getInitializer();
350
+ if (!v)
351
+ continue;
352
+ if (v.getKind() === SyntaxKind.ArrayLiteralExpression) {
353
+ for (const el of v.getElements()) {
354
+ if (isCallableArg(el))
355
+ optsMiddleware.push(el);
356
+ }
357
+ }
358
+ else if (isCallableArg(v)) {
359
+ optsMiddleware.push(v);
360
+ }
361
+ }
362
+ continue;
363
+ }
364
+ if (arg.getKind() === SyntaxKind.ArrayLiteralExpression) {
365
+ for (const el of arg.getElements()) {
366
+ if (isCallableArg(el))
367
+ callables.push(el);
368
+ }
369
+ continue;
370
+ }
371
+ if (isCallableArg(arg))
372
+ callables.push(arg);
373
+ }
374
+ // Terminal handler: a Fastify opts `handler` wins; else the last positional.
375
+ let terminal;
376
+ let middleware;
377
+ if (optsHandler) {
378
+ terminal = optsHandler;
379
+ middleware = [...callables, ...optsMiddleware];
380
+ }
381
+ else if (callables.length) {
382
+ terminal = callables[callables.length - 1];
383
+ middleware = [...callables.slice(0, -1), ...optsMiddleware];
384
+ }
385
+ else {
386
+ terminal = undefined;
387
+ middleware = optsMiddleware;
388
+ }
389
+ if (!terminal) {
390
+ for (const l of labels)
391
+ acc.diag.unresolvedHandlers.add(`${l.method} ${l.path}`);
392
+ }
393
+ else {
394
+ const tFile = resolveHandlerFile(terminal, imports, selfFileId, resolveSpecifier);
395
+ if (tFile === null) {
396
+ for (const l of labels)
397
+ acc.diag.unresolvedHandlers.add(`${l.method} ${l.path}`);
398
+ }
399
+ else {
400
+ addEdge(acc, selfFileId, tFile, { framework: 'node', relation: 'route' });
401
+ addRole(acc, tFile, 'route-handler');
402
+ for (const l of labels)
403
+ addRouteLabel(acc, tFile, l);
404
+ // record the registration for route-prefix grouping. The path(s)
405
+ // (deduped) + the registering controller's mount prefix yield the subsystem.
406
+ const paths = [...new Set(labels.map((l) => l.path))];
407
+ if (paths.length)
408
+ acc.routeRegs.push({ controller: selfFileId, handler: tFile, paths });
409
+ }
410
+ }
411
+ for (const mw of middleware) {
412
+ const mFile = resolveHandlerFile(mw, imports, selfFileId, resolveSpecifier);
413
+ if (mFile === null)
414
+ continue; // an external middleware factory — not on-graph
415
+ addEdge(acc, selfFileId, mFile, { framework: 'node', relation: 'middleware' });
416
+ addRole(acc, mFile, 'middleware');
417
+ }
418
+ }
419
+ // Scan one source file for the request spine.
420
+ function scanFile(sf, selfFileId, imports, resolveSpecifier, acc) {
421
+ // Entrypoint: `new Koa()` / `new Hono()` (and `new Fastify()`).
422
+ // Controller: `new Router()` (Koa @koa/router used as a class).
423
+ for (const ne of sf.getDescendantsOfKind(SyntaxKind.NewExpression)) {
424
+ const cls = ne.getExpression().getText();
425
+ if (FACTORY_NEW_IDENTS.has(cls))
426
+ addRole(acc, selfFileId, 'entrypoint');
427
+ else if (CONTROLLER_NEW_IDENTS.has(cls))
428
+ addRole(acc, selfFileId, 'controller');
429
+ }
430
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
431
+ const callee = call.getExpression();
432
+ // Entrypoint: `express()` / `fastify()` app factories.
433
+ if (callee.getKind() === SyntaxKind.Identifier && FACTORY_CALL_IDENTS.has(callee.getText())) {
434
+ addRole(acc, selfFileId, 'entrypoint');
435
+ }
436
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression)
437
+ continue;
438
+ const method = callee.getName();
439
+ const args = call.getArguments();
440
+ // Entrypoint: `….listen(port)` server bootstrap. Matched on the method name
441
+ // alone (any receiver) ON PURPOSE: the common `import app from './app';
442
+ // app.listen(3000)` index file has NO app-factory of its own, so requiring a
443
+ // co-located factory would miss that legitimate entrypoint. The cost is a
444
+ // rare over-tag (a non-server `.listen()` — an EventEmitter, a net.Server),
445
+ // which is low-risk inside an already-detected Node-backend repo.
446
+ if (method === 'listen') {
447
+ addRole(acc, selfFileId, 'entrypoint');
448
+ continue;
449
+ }
450
+ // Controller: `express.Router()` / `Router()` router-module creator.
451
+ if (method === 'Router') {
452
+ addRole(acc, selfFileId, 'controller');
453
+ continue;
454
+ }
455
+ // Mount: Hono `app.route('/prefix', subApp)` — arg0 string path, arg1 a
456
+ // sub-app identifier ⇒ the mounted file is a controller. (Fastify
457
+ // `app.route({…})` is the object form, handled below.)
458
+ if (method === 'route') {
459
+ if (args.length >= 2 && staticString(args[0]) !== undefined && isCallableArg(args[1])) {
460
+ const tFile = resolveHandlerFile(args[1], imports, selfFileId, resolveSpecifier);
461
+ if (tFile !== null) {
462
+ addEdge(acc, selfFileId, tFile, { framework: 'node', relation: 'mount' });
463
+ addRole(acc, tFile, 'controller');
464
+ acc.mountTargets.add(tFile); // override entrypoint→controller post-scan
465
+ // the mount path prefixes the sub-app's routes.
466
+ acc.mounts.push({ parent: selfFileId, child: tFile, mountPath: staticString(args[0]) });
467
+ }
468
+ continue;
469
+ }
470
+ // Fastify `app.route({ method, url, handler, … })`.
471
+ if (args.length >= 1 && args[0].getKind() === SyntaxKind.ObjectLiteralExpression) {
472
+ scanFastifyRouteObject(args[0], selfFileId, imports, resolveSpecifier, acc);
473
+ continue;
474
+ }
475
+ // Express chained `app.route('/x').get(h)` and friends — can't read the
476
+ // verb statically off the chain. Degrade + count (no silent drop).
477
+ acc.diag.unsupported++;
478
+ continue;
479
+ }
480
+ // `app.use(…)` middleware chain (and path-mounted middleware/routers).
481
+ if (method === 'use') {
482
+ let rest = args;
483
+ let mountPath;
484
+ if (rest.length && staticString(rest[0]) !== undefined) {
485
+ mountPath = staticString(rest[0]); // static mount path → prefixes the target
486
+ rest = rest.slice(1);
487
+ }
488
+ else if (
489
+ // `app.use(computedPath, router)`: a non-static, non-callable,
490
+ // non-object first arg followed by a handler is a DYNAMIC mount path we
491
+ // can't resolve to a prefix. Degrade + log (no silent drop).
492
+ args.length >= 2 &&
493
+ !isCallableArg(args[0]) &&
494
+ args[0].getKind() !== SyntaxKind.ObjectLiteralExpression &&
495
+ args.slice(1).some(isCallableArg)) {
496
+ acc.diag.dynamicMounts++;
497
+ }
498
+ for (const a of rest) {
499
+ if (!isCallableArg(a))
500
+ continue;
501
+ const mFile = resolveHandlerFile(a, imports, selfFileId, resolveSpecifier);
502
+ if (mFile === null)
503
+ continue;
504
+ addEdge(acc, selfFileId, mFile, { framework: 'node', relation: 'use' });
505
+ // Path-mounted routers become controllers via the express.Router() pass;
506
+ // here we tag middleware and let the role-priority collapse keep the
507
+ // stronger controller role if both fire.
508
+ addRole(acc, mFile, 'middleware');
509
+ // a static-path `use` is a candidate router mount; the grouping
510
+ // phase keeps only the ones whose target turned out to be a controller.
511
+ if (mountPath !== undefined)
512
+ acc.mounts.push({ parent: selfFileId, child: mFile, mountPath });
513
+ }
514
+ continue;
515
+ }
516
+ // Hono `app.on('GET', '/x', …)` / `app.on(['GET','POST'], '/x', …)`.
517
+ if (method === 'on') {
518
+ if (args.length < 2)
519
+ continue;
520
+ const methods = resolveOnMethods(args[0]);
521
+ const paths = resolvePaths(args[1]);
522
+ if (!methods) {
523
+ acc.diag.dynamicPaths++; // dynamic method spec — treat like a dynamic reg
524
+ continue;
525
+ }
526
+ if (!paths) {
527
+ acc.diag.dynamicPaths++;
528
+ continue;
529
+ }
530
+ const labels = [];
531
+ for (const m of methods)
532
+ for (const p of paths)
533
+ labels.push({ method: m, path: p });
534
+ processHandlerChain(args.slice(2), selfFileId, labels, imports, resolveSpecifier, acc);
535
+ continue;
536
+ }
537
+ // `app.METHOD(path, …handlers)` route registrations.
538
+ if (HTTP_METHODS.has(method)) {
539
+ // The ≥2-arg + callable-handler rule rejects Express config reads
540
+ // (`app.get('title')`). A registration needs a path + ≥1 handler.
541
+ if (args.length < 2)
542
+ continue;
543
+ if (!args.slice(1).some(isCallableArg) && !args.slice(1).some((a) => a.getKind() === SyntaxKind.ObjectLiteralExpression)) {
544
+ continue;
545
+ }
546
+ const paths = resolvePaths(args[0]);
547
+ if (!paths) {
548
+ acc.diag.dynamicPaths++;
549
+ continue;
550
+ }
551
+ const verb = method.toUpperCase();
552
+ const labels = paths.map((p) => ({ method: verb, path: p }));
553
+ processHandlerChain(args.slice(1), selfFileId, labels, imports, resolveSpecifier, acc);
554
+ continue;
555
+ }
556
+ }
557
+ }
558
+ // Fastify `app.route({ method, url|path, handler, preHandler, … })`.
559
+ function scanFastifyRouteObject(obj, selfFileId, imports, resolveSpecifier, acc) {
560
+ const urlProp = obj.getProperty('url') ?? obj.getProperty('path');
561
+ const urlInit = urlProp && urlProp.getKind() === SyntaxKind.PropertyAssignment
562
+ ? urlProp.getInitializer()
563
+ : undefined;
564
+ const paths = urlInit ? resolvePaths(urlInit) : null;
565
+ if (!paths) {
566
+ acc.diag.dynamicPaths++;
567
+ return;
568
+ }
569
+ const methods = resolveRouteObjectMethods(obj);
570
+ const labels = [];
571
+ for (const m of methods)
572
+ for (const p of paths)
573
+ labels.push({ method: m, path: p });
574
+ // Reuse the chain processor by handing it the object itself (it pulls
575
+ // `handler` + the hook keys out of the options object).
576
+ processHandlerChain([obj], selfFileId, labels, imports, resolveSpecifier, acc);
577
+ }
578
+ // `method` of a Fastify route object: a static string, an array of static
579
+ // strings, or — if unreadable — 'ALL' (so the route still registers a label).
580
+ function resolveRouteObjectMethods(obj) {
581
+ const p = obj.getProperty('method');
582
+ if (!p || p.getKind() !== SyntaxKind.PropertyAssignment)
583
+ return ['ALL'];
584
+ const init = p.getInitializer();
585
+ if (!init)
586
+ return ['ALL'];
587
+ const single = staticString(init);
588
+ if (single !== undefined)
589
+ return [single.toUpperCase()];
590
+ if (init.getKind() === SyntaxKind.ArrayLiteralExpression) {
591
+ const out = [];
592
+ for (const el of init.getElements()) {
593
+ const v = staticString(el);
594
+ if (v !== undefined)
595
+ out.push(v.toUpperCase());
596
+ }
597
+ if (out.length)
598
+ return out;
599
+ }
600
+ return ['ALL'];
601
+ }
602
+ // Hono `app.on(method, …)` first arg → method list (a string or string array),
603
+ // or null when computed.
604
+ function resolveOnMethods(arg) {
605
+ const single = staticString(arg);
606
+ if (single !== undefined)
607
+ return [single.toUpperCase()];
608
+ if (arg.getKind() === SyntaxKind.ArrayLiteralExpression) {
609
+ const out = [];
610
+ for (const el of arg.getElements()) {
611
+ const v = staticString(el);
612
+ if (v === undefined)
613
+ return null;
614
+ out.push(v.toUpperCase());
615
+ }
616
+ return out.length ? out : null;
617
+ }
618
+ return null;
619
+ }
620
+ // ---------------------------------------------------------------------------
621
+ // route-prefix / router-mount grouping (deterministic, no LLM).
622
+ // Deterministic slug for a group id / path segment (camelCase → kebab, drop
623
+ // non-alnum). Mirrors the Nest adapter's slug discipline (a private copy — the
624
+ // carve-out keeps each adapter self-contained).
625
+ function slugify(s) {
626
+ return s
627
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
628
+ .toLowerCase()
629
+ .replace(/[^a-z0-9]+/g, '-')
630
+ .replace(/^-+|-+$/g, '');
631
+ }
632
+ // Humanize a slug into a subsystem label (`users` → 'Users', `user-profiles` →
633
+ // 'User Profiles'). Falls back to the slug when it has no word characters.
634
+ function humanizeSlug(slug) {
635
+ const words = slug.split('-').filter(Boolean);
636
+ if (words.length === 0)
637
+ return slug;
638
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
639
+ }
640
+ // A LITERAL path segment (not a route param / wildcard / regex). Express,
641
+ // Fastify and Hono params start with `:`; wildcards are `*`; some forms use
642
+ // `{…}` / `(…)`. Only plain literal segments carry a grouping signal.
643
+ function isStaticSegment(seg) {
644
+ return /^[A-Za-z0-9._~-]+$/.test(seg);
645
+ }
646
+ // Leading "noise" segments that carry no grouping signal — a shared `/api`
647
+ // prefix, or an API version (`/v1`, `/v2`). Stripping them turns `/api/users` +
648
+ // `/api/orders` into the distinguishing `users` / `orders` subsystems.
649
+ //
650
+ // DELIBERATELY NARROW: only the `api` + version vocabulary, never a domain-ish
651
+ // word (a literal `/rest/...` or `/admin/...` is kept as the group key, because
652
+ // it could genuinely be the feature name). This is the "minimal noise" half of a
653
+ // GLOBAL-per-path strip — it is NOT sibling-relative, so an app whose routes ALL
654
+ // sit under one shared NON-noise prefix (`/admin/users`, `/admin/orders`)
655
+ // intentionally collapses to a single `admin` subsystem rather than splitting on
656
+ // the next segment. That matches the mount example (`app.use('/admin', router)`
657
+ // → `admin`); recovering sibling-relative splits would need cross-route analysis
658
+ // we deliberately skip here.
659
+ const NOISE_SEGMENTS = new Set(['api']);
660
+ function isNoiseSegment(seg) {
661
+ const s = seg.toLowerCase();
662
+ return NOISE_SEGMENTS.has(s) || /^v\d+$/.test(s);
663
+ }
664
+ // The grouping slug for a full route path: the first DISTINGUISHING static
665
+ // segment (leading noise segments skipped while a non-noise one remains).
666
+ // Undefined when the path has no static segment at all (e.g. `/` or `/:id`) —
667
+ // such a route is ungroupable (degrade + log at the call site).
668
+ function prefixSlug(fullPath) {
669
+ const statics = fullPath.split('/').filter((s) => s.length > 0 && isStaticSegment(s));
670
+ if (statics.length === 0)
671
+ return undefined;
672
+ let i = 0;
673
+ while (i < statics.length - 1 && isNoiseSegment(statics[i]))
674
+ i++;
675
+ return slugify(statics[i]) || undefined;
676
+ }
677
+ // Join a mount prefix with a (mount or route) path into one normalized posix
678
+ // path. `joinPath('', '/users')` → '/users'; `joinPath('/api/users', '/:id')`
679
+ // → '/api/users/:id'; empty result → '/'.
680
+ function joinPath(a, b) {
681
+ const segs = [...a.split('/'), ...b.split('/')].filter((s) => s.length > 0);
682
+ return '/' + segs.join('/');
683
+ }
684
+ // Resolve each mounted controller's MOUNT PREFIX from the mount graph, seeded by
685
+ // the entrypoints (the server root is mounted at ''). Only mounts whose CHILD is
686
+ // a controller (a real sub-router) propagate a prefix. The fixpoint keeps the
687
+ // lexicographically-smallest prefix reachable from an entrypoint — ORDER-
688
+ // INDEPENDENT, so the result is deterministic regardless of scan order. A
689
+ // controller mounted at several distinct prefixes is reported in `multiMounted`
690
+ // (logged; smallest kept). A controller never reached from an entrypoint stays
691
+ // absent → the caller falls back to '' (its routes group by their own path).
692
+ function resolveMountPrefixes(entrypoints, controllers, mounts) {
693
+ const prefixByFile = new Map();
694
+ for (const e of [...entrypoints].sort())
695
+ prefixByFile.set(e, '');
696
+ const ctrlMounts = mounts
697
+ .filter((m) => controllers.has(m.child))
698
+ .sort((a, b) => a.parent < b.parent
699
+ ? -1
700
+ : a.parent > b.parent
701
+ ? 1
702
+ : a.child < b.child
703
+ ? -1
704
+ : a.child > b.child
705
+ ? 1
706
+ : a.mountPath < b.mountPath
707
+ ? -1
708
+ : a.mountPath > b.mountPath
709
+ ? 1
710
+ : 0);
711
+ const cap = controllers.size + ctrlMounts.length + 2; // bounds any mount cycle
712
+ for (let round = 0; round < cap; round++) {
713
+ let changed = false;
714
+ for (const m of ctrlMounts) {
715
+ const pPrefix = prefixByFile.get(m.parent);
716
+ if (pPrefix === undefined)
717
+ continue;
718
+ const cand = joinPath(pPrefix, m.mountPath);
719
+ const cur = prefixByFile.get(m.child);
720
+ if (cur === undefined || cand < cur) {
721
+ prefixByFile.set(m.child, cand);
722
+ changed = true;
723
+ }
724
+ }
725
+ if (!changed)
726
+ break;
727
+ }
728
+ // Multi-mount detection (logging only): a child resolvable to ≥2 distinct
729
+ // candidate prefixes (the smallest already won above).
730
+ const candsByChild = new Map();
731
+ for (const m of ctrlMounts) {
732
+ const pPrefix = prefixByFile.get(m.parent);
733
+ if (pPrefix === undefined)
734
+ continue;
735
+ const cand = joinPath(pPrefix, m.mountPath);
736
+ let set = candsByChild.get(m.child);
737
+ if (!set) {
738
+ set = new Set();
739
+ candsByChild.set(m.child, set);
740
+ }
741
+ set.add(cand);
742
+ }
743
+ const multiMounted = new Set();
744
+ for (const [child, set] of candsByChild)
745
+ if (set.size > 1)
746
+ multiMounted.add(child);
747
+ return { prefixByFile, multiMounted };
748
+ }
749
+ // Build the route-prefix grouping priors from the accumulated registrations +
750
+ // mounts + service tail. PURE over `acc.roleByFile` (final, post pass-2). The
751
+ // entrypoint is never grouped (it spans every prefix). Returns sorted, stable
752
+ // groups + the degrade counts for logging.
753
+ function buildRouteGroups(acc) {
754
+ const entrypoints = new Set();
755
+ const controllers = new Set();
756
+ for (const [f, role] of acc.roleByFile) {
757
+ if (role === 'entrypoint')
758
+ entrypoints.add(f);
759
+ else if (role === 'controller')
760
+ controllers.add(f);
761
+ }
762
+ const { prefixByFile, multiMounted } = resolveMountPrefixes(entrypoints, controllers, acc.mounts);
763
+ const groupBySlug = new Map();
764
+ const slugsByFile = new Map();
765
+ // Never group the server root: it serves every prefix, so forcing it into one
766
+ // route subsystem would be wrong. It keeps its directory/own subsystem.
767
+ const addToGroup = (slug, fileId) => {
768
+ if (acc.roleByFile.get(fileId) === 'entrypoint')
769
+ return;
770
+ let g = groupBySlug.get(slug);
771
+ if (!g) {
772
+ g = { label: humanizeSlug(slug), files: new Set() };
773
+ groupBySlug.set(slug, g);
774
+ }
775
+ g.files.add(fileId);
776
+ let s = slugsByFile.get(fileId);
777
+ if (!s) {
778
+ s = new Set();
779
+ slugsByFile.set(fileId, s);
780
+ }
781
+ s.add(slug);
782
+ };
783
+ let ungroupable = 0;
784
+ for (const reg of acc.routeRegs) {
785
+ const ctrlPrefix = prefixByFile.get(reg.controller) ?? '';
786
+ const ctrlIsRouter = acc.roleByFile.get(reg.controller) === 'controller';
787
+ const slugs = new Set();
788
+ for (const p of reg.paths) {
789
+ const slug = prefixSlug(joinPath(ctrlPrefix, p));
790
+ if (!slug) {
791
+ ungroupable++;
792
+ continue;
793
+ }
794
+ slugs.add(slug);
795
+ }
796
+ for (const slug of slugs) {
797
+ addToGroup(slug, reg.handler);
798
+ // A sub-router (controller) belongs to its own prefix's subsystem; an
799
+ // entrypoint registering an inline route is excluded by addToGroup.
800
+ if (ctrlIsRouter)
801
+ addToGroup(slug, reg.controller);
802
+ }
803
+ }
804
+ // A mounted controller with no direct routes of its own (it only re-mounts
805
+ // deeper) still belongs to its mount prefix's subsystem.
806
+ for (const c of controllers) {
807
+ const pre = prefixByFile.get(c);
808
+ if (pre === undefined || pre === '')
809
+ continue;
810
+ const slug = prefixSlug(pre);
811
+ if (slug)
812
+ addToGroup(slug, c);
813
+ }
814
+ // The one-hop service tail joins the route group(s) of the handler that calls
815
+ // it (a shared service claimed by several groups is arbitrated by the
816
+ // contribute-step's majority-claim rule).
817
+ for (const st of acc.serviceTail) {
818
+ const callerSlugs = slugsByFile.get(st.handler);
819
+ if (!callerSlugs)
820
+ continue;
821
+ for (const slug of callerSlugs)
822
+ addToGroup(slug, st.service);
823
+ }
824
+ const groups = [...groupBySlug.entries()]
825
+ .map(([id, g]) => ({ id, label: g.label, fileIds: [...g.files].sort() }))
826
+ .filter((g) => g.fileIds.length > 0)
827
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
828
+ return { groups, multiMounted, ungroupable };
829
+ }
830
+ // ---------------------------------------------------------------------------
831
+ // The full analysis.
832
+ function analyzeNode(ctx) {
833
+ const { repoDir, rootPath, graph } = ctx;
834
+ const project = buildExtractionProject(repoDir);
835
+ addAllSourceFiles(project, repoDir);
836
+ const fileById = new Map();
837
+ for (const sf of project.getSourceFiles())
838
+ fileById.set(toId(repoDir, sf.getFilePath()), sf);
839
+ const idSet = new Set(fileById.keys());
840
+ const resolveSpecifier = makeSpecifierResolver(idSet);
841
+ const acc = {
842
+ roleByFile: new Map(),
843
+ routesByFile: new Map(),
844
+ edges: new Map(),
845
+ mountTargets: new Set(),
846
+ routeRegs: [],
847
+ mounts: [],
848
+ serviceTail: [],
849
+ diag: { dynamicPaths: 0, unresolvedHandlers: new Set(), unsupported: 0, dynamicMounts: 0 },
850
+ };
851
+ // Pass 1 — roles + route/middleware/mount edges from the registration sites.
852
+ for (const [fileId, sf] of fileById) {
853
+ if (!inScope(fileId, rootPath))
854
+ continue;
855
+ const imports = buildImportNameMap(sf, repoDir);
856
+ scanFile(sf, fileId, imports, (spec) => resolveSpecifier(fileId, spec), acc);
857
+ }
858
+ // A mounted app instance is a controller, even when constructed like the root
859
+ // app (`new Hono()` → `entrypoint`). The root app is never a mount target, so
860
+ // this override only ever demotes a sub-app — never the real entrypoint.
861
+ for (const f of acc.mountTargets)
862
+ acc.roleByFile.set(f, 'controller');
863
+ // Pass 2 — handler → service tail. A service is an INTERNAL file a request
864
+ // handler (route-handler / controller) directly CALLS (a structural `call`
865
+ // edge — NOT a mere import) per the graph, that isn't itself a request-spine
866
+ // role. The edge is already structural (deduped downstream); re-emitting it
867
+ // carries the spine + drives the `service` role. One hop only — the immediate
868
+ // tail. We gate on `call` (not `import`) so the spec's "handler → service
869
+ // CALLS" is honored precisely: a real call (`svc.foo()`) always emits a `call`
870
+ // edge, while a type-only / constant import emits only an `import` edge — so
871
+ // this loses no real tail but never mislabels a pure type/constant dep as a
872
+ // service.
873
+ const handlerFiles = new Set();
874
+ for (const [fileId, role] of acc.roleByFile) {
875
+ if (role === 'route-handler' || role === 'controller')
876
+ handlerFiles.add(fileId);
877
+ }
878
+ if (handlerFiles.size > 0) {
879
+ for (const e of graph.edges) {
880
+ if (e.external)
881
+ continue;
882
+ if (e.kind !== 'call')
883
+ continue; // CALLS only — not type/constant imports
884
+ if (e.from === e.to)
885
+ continue;
886
+ if (!handlerFiles.has(e.from))
887
+ continue;
888
+ if (!inScope(e.to, rootPath))
889
+ continue;
890
+ const targetRole = acc.roleByFile.get(e.to);
891
+ // Skip request-spine targets (don't relabel an entry/handler/mw/controller
892
+ // a service); an as-yet-unroled internal target is the service tail.
893
+ if (targetRole === 'entrypoint' ||
894
+ targetRole === 'route-handler' ||
895
+ targetRole === 'controller' ||
896
+ targetRole === 'middleware') {
897
+ continue;
898
+ }
899
+ addEdge(acc, e.from, e.to, { framework: 'node', relation: 'service' });
900
+ addRole(acc, e.to, 'service');
901
+ // the service tail; the service joins its caller's route group(s).
902
+ acc.serviceTail.push({ handler: e.from, service: e.to });
903
+ }
904
+ }
905
+ // Build the per-file role tags, folding accumulated route labels onto the
906
+ // WINNING role's metadata (so a route served inline by the entrypoint still
907
+ // carries its method+path label even though `entrypoint` outranks
908
+ // `route-handler`). Route labels sorted for snapshot stability.
909
+ //
910
+ // NOTE (per-MODULE collapse): these are per-FILE tags. When several handler
911
+ // files land in ONE module, the contribute-step keeps a single RoleTag per
912
+ // module (highest priority, lexical tiebreak), so only the winning file's
913
+ // `routes` metadata survives at module granularity — the per-module route
914
+ // table is best-effort, not exhaustive. That's an accepted limitation of the
915
+ // LIGHTWEIGHT slice (route labels are metadata, not nodes); a richer per-module
916
+ // route aggregation would belong with the deferred grouping work.
917
+ const roles = new Map();
918
+ for (const [fileId, role] of acc.roleByFile) {
919
+ const tag = { role, kind: ROLE_KIND[role], priority: ROLE_PRIORITY[role] };
920
+ const routes = acc.routesByFile.get(fileId);
921
+ const meta = { framework: 'node' };
922
+ if (routes && routes.length) {
923
+ meta.routes = [...routes].sort((a, b) => a.method < b.method ? -1 : a.method > b.method ? 1 : a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
924
+ }
925
+ tag.metadata = meta;
926
+ roles.set(fileId, tag);
927
+ }
928
+ // Deterministic edge ordering — module-id resolution + dedupe downstream rely
929
+ // on a stable input order; sort by endpoints.
930
+ const edges = [...acc.edges.values()].sort((a, b) => a.source < b.source ? -1 : a.source > b.source ? 1 : a.target < b.target ? -1 : a.target > b.target ? 1 : 0);
931
+ // the route-prefix / router-mount grouping priors.
932
+ const { groups, multiMounted, ungroupable } = buildRouteGroups(acc);
933
+ // Positive signal for validation.
934
+ if (acc.roleByFile.size > 0 || edges.length > 0) {
935
+ console.log(` [node] request spine: ${edges.length} edge(s) · ${acc.roleByFile.size} role(s) across the request flow`);
936
+ }
937
+ if (groups.length > 0) {
938
+ console.log(` [node] grouping: ${groups.length} route-prefix subsystem(s) [${groups.map((g) => g.id).join(', ')}]`);
939
+ }
940
+ // No silent caps (locked): log everything that degraded.
941
+ if (acc.diag.dynamicPaths > 0 || acc.diag.unresolvedHandlers.size > 0 || acc.diag.unsupported > 0) {
942
+ const parts = [];
943
+ if (acc.diag.dynamicPaths > 0)
944
+ parts.push(`${acc.diag.dynamicPaths} dynamic/computed route path(s)`);
945
+ if (acc.diag.unresolvedHandlers.size > 0) {
946
+ parts.push(`${acc.diag.unresolvedHandlers.size} route(s) with an unresolvable handler: ${[...acc.diag.unresolvedHandlers].sort().join(', ')}`);
947
+ }
948
+ if (acc.diag.unsupported > 0)
949
+ parts.push(`${acc.diag.unsupported} unsupported route form(s)`);
950
+ console.log(` [node] skipped ${parts.join(' · ')} (logged, not silently dropped)`);
951
+ }
952
+ // No silent caps (locked): log grouping degradations too.
953
+ if (ungroupable > 0 || acc.diag.dynamicMounts > 0 || multiMounted.size > 0) {
954
+ const parts = [];
955
+ if (ungroupable > 0)
956
+ parts.push(`${ungroupable} route(s) with no static prefix (ungrouped)`);
957
+ if (acc.diag.dynamicMounts > 0)
958
+ parts.push(`${acc.diag.dynamicMounts} dynamic/computed mount path(s)`);
959
+ if (multiMounted.size > 0) {
960
+ parts.push(`${multiMounted.size} controller(s) mounted at multiple prefixes (kept lexically-smallest)`);
961
+ }
962
+ console.log(` [node] grouping skipped ${parts.join(' · ')} (logged, not silently dropped)`);
963
+ }
964
+ return { edges, roles, groups };
965
+ }
966
+ function getAnalysis(ctx) {
967
+ let a = ANALYSIS_CACHE.get(ctx);
968
+ if (!a) {
969
+ a = analyzeNode(ctx);
970
+ ANALYSIS_CACHE.set(ctx, a);
971
+ }
972
+ return a;
973
+ }
974
+ // ---------------------------------------------------------------------------
975
+ // The adapter.
976
+ export const nodeAdapter = {
977
+ name: 'node',
978
+ async detect(ctx) {
979
+ const { base, rootPath } = resolveBase(ctx);
980
+ return scoreNode(gatherNodeSignals(base), rootPath);
981
+ },
982
+ // the route-prefix / router-mount grouping prior (file-id space; the
983
+ // contribute-step resolves to modules + overrides their subsystem, AUTHORITATIVE
984
+ // over directory + workspace-package grouping). Deterministic `node:<slug>` ids
985
+ // (prefix-derived, never an index). No classificationsNeeded — grouping is
986
+ // fully deterministic; ungroupable routes / dynamic mounts degrade + log inside
987
+ // the analysis, so the deferred-classification channel stays empty.
988
+ async groupingPrior(ctx) {
989
+ return { groups: getAnalysis(ctx).groups };
990
+ },
991
+ // the request spine as roles + edges (file-id space; the
992
+ // contribution step resolves to modules, drops self-edges, dedupes,
993
+ // 8-verb-validates). kind 'calls' only.
994
+ async syntheticEdges(ctx) {
995
+ return getAnalysis(ctx).edges;
996
+ },
997
+ async roleTags(ctx) {
998
+ return getAnalysis(ctx).roles;
999
+ },
1000
+ // The hooks READ SOURCE (route registrations, app.use chains, handler bodies),
1001
+ // so declare the source paths the diff-driven hosted walk must treat as
1002
+ // framework-relevant. Never-store-source holds: read server-side, persist only
1003
+ // the derived edges/roles.
1004
+ scansSourcePath(path) {
1005
+ const ext = path.split('.').pop();
1006
+ return ext !== undefined && SOURCE_EXT_SET.has(ext);
1007
+ },
1008
+ };
1009
+ //# sourceMappingURL=node.js.map