@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,940 @@
1
+ // the Django FrameworkAdapter. The densest Python adapter, built on the
2
+ // FastAPI reference and the shared Python core ( foundation:
3
+ // framework/python/{py-ast,analyze}.ts). Net-new; detects against pyproject.toml /
4
+ // requirements*.txt (the `django` dep) or a manage.py + settings module, NOT
5
+ // package.json.
6
+ //
7
+ // Django DECLARES its architecture structurally, which we read STATICALLY
8
+ // (install-free, never-store-source — parse server-side, persist only the derived
9
+ // groups/edges/roles) via a pure syntactic Pyright parse (../python/py-ast.ts):
10
+ //
11
+ // * detect() — the `django` dependency, or a `manage.py` + a settings
12
+ // module (django-rest-framework / django-ninja noted for
13
+ // the API passes). Shallow nested-backend detection too.
14
+ // * groupingPrior — THE HEADLINE: each Django app (a package dir with apps.py /
15
+ // models.py / an AppConfig) becomes its own subsystem, so a
16
+ // flat `apps/` folder splits into per-domain subsystems
17
+ // (Users / Orders / Catalog) — the strongest first-party
18
+ // signal Django gives. Same mechanism the Nest @Module / RN
19
+ // navigator priors use: the contribute-step makes each group
20
+ // its own subsystem, authoritative over the directory heuristic.
21
+ // * syntheticEdges — the wiring the import graph doesn't name as verbs:
22
+ // urls.py URLconf `path()`/`re_path()`/`url()` → the view it
23
+ // routes to + `include()` sub-URLconf mounting (kind 'calls');
24
+ // `models.Model` FK / OneToOne / ManyToMany → a data
25
+ // relationship between the two model modules (kind 'calls' —
26
+ // the safe locked verb for a FK); Django signals
27
+ // (`@receiver` / `signal.connect` / `signal.send`) →
28
+ // publishes/subscribes; DRF `router.register(prefix, ViewSet)`
29
+ // + Django Ninja `api.add_router(prefix, router)` mounting
30
+ // (kind 'calls').
31
+ // * roleTags — function/class-based views + DRF ViewSet + Ninja operations
32
+ // → `gateway`; a models module → `service` (role 'model');
33
+ // `management/commands/*.py` Command classes → `job`; an
34
+ // AppConfig → `service`. METADATA onto the LOCKED MODULE_KINDS
35
+ // enum; never a new kind (the module's `kind` is unchanged —
36
+ // only `role` is rendered).
37
+ //
38
+ // Unresolvable url views / includes / FK targets / router registrations / signals
39
+ // DEGRADE + LOG — no silent caps. Everything is deterministic (sorted outputs, ids
40
+ // derived from names/paths, lexical tiebreaks; run-twice is byte-identical) and
41
+ // never executes repo code (pure Pyright parse, no venv/subprocess/import).
42
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
43
+ import { join } from 'node:path';
44
+ import { clampConfidence, resolveBase } from '../detect-util.js';
45
+ import { readPythonDeps, requirementName } from '../../graph/python-manifest.js';
46
+ import { inferSourceRoots, syntacticResolve } from '../../graph/python-adapter.js';
47
+ import { buildImportBindings, inScope, isPythonFile } from '../python/analyze.js';
48
+ import { callCallee, collectNodes, keywordArg, memberChain, nameValue, parsePython, positionalArgs, stringValue, PN, } from '../python/py-ast.js';
49
+ import { ParseTreeWalker } from '@zzzen/pyright-internal/dist/analyzer/parseTreeWalker.js';
50
+ // Non-source dirs the nested + settings scans skip (cheap; can't hold a manifest).
51
+ const SKIP_DIRS = new Set([
52
+ 'node_modules',
53
+ 'dist',
54
+ 'build',
55
+ 'out',
56
+ 'coverage',
57
+ 'venv',
58
+ '.venv',
59
+ '__pycache__',
60
+ 'site-packages',
61
+ 'migrations',
62
+ 'static',
63
+ 'templates',
64
+ 'media',
65
+ ]);
66
+ /**
67
+ * True if `dir` holds a Django project module: a settings module (settings.py /
68
+ * settings/ package), or the WSGI/ASGI entrypoints `django-admin startproject`
69
+ * generates next to it. Real projects name settings freely (`config/django/base.py`),
70
+ * so the co-located `wsgi.py` / `asgi.py` are the reliable existence-only signal.
71
+ */
72
+ function hasProjectModule(dir) {
73
+ return (existsSync(join(dir, 'settings.py')) ||
74
+ existsSync(join(dir, 'settings', '__init__.py')) ||
75
+ existsSync(join(dir, 'wsgi.py')) ||
76
+ existsSync(join(dir, 'asgi.py')));
77
+ }
78
+ /**
79
+ * Does a Django project module exist at `base` or one directory down? The config
80
+ * package (`config/` | `<project>/`) canonically holds it, so a shallow scan of
81
+ * `base` + its immediate subdirs finds it without reading source.
82
+ */
83
+ function findsProjectModule(base) {
84
+ if (hasProjectModule(base))
85
+ return true;
86
+ let entries;
87
+ try {
88
+ entries = readdirSync(base, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ for (const e of entries) {
94
+ if (!e.isDirectory() || e.name.startsWith('.') || SKIP_DIRS.has(e.name))
95
+ continue;
96
+ if (hasProjectModule(join(base, e.name)))
97
+ return true;
98
+ }
99
+ return false;
100
+ }
101
+ /**
102
+ * Every declared dep name at `baseDir`, unioning `readPythonDeps` (pyproject +
103
+ * root requirements*.txt) with a `requirements/` DIRECTORY (`base.txt` / `local.txt`
104
+ * / `production.txt` — the common Django split that the root helper's
105
+ * `requirements*.txt` glob misses). Membership only; deterministic; never source.
106
+ */
107
+ function gatherDjangoDeps(baseDir) {
108
+ const deps = readPythonDeps(baseDir);
109
+ let entries;
110
+ try {
111
+ entries = readdirSync(join(baseDir, 'requirements'));
112
+ }
113
+ catch {
114
+ return deps;
115
+ }
116
+ for (const entry of entries) {
117
+ if (!entry.endsWith('.txt'))
118
+ continue;
119
+ let text;
120
+ try {
121
+ text = readFileSync(join(baseDir, 'requirements', entry), 'utf8');
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ for (const line of text.split('\n')) {
127
+ const n = requirementName(line);
128
+ if (n)
129
+ deps.add(n);
130
+ }
131
+ }
132
+ return deps;
133
+ }
134
+ /** Gather the signal set for a single root dir (reads manifests + file existence). */
135
+ export function gatherDjangoSignals(baseDir) {
136
+ const deps = gatherDjangoDeps(baseDir);
137
+ return {
138
+ hasDjango: deps.has('django'),
139
+ hasDrf: deps.has('djangorestframework'),
140
+ hasNinja: deps.has('django-ninja'),
141
+ hasManagePy: existsSync(join(baseDir, 'manage.py')),
142
+ hasSettings: findsProjectModule(baseDir),
143
+ };
144
+ }
145
+ /** True if `dir` holds a Python manifest OR the Django launcher — worth a scan. */
146
+ function hasDjangoScanTarget(dir) {
147
+ return (existsSync(join(dir, 'pyproject.toml')) ||
148
+ existsSync(join(dir, 'setup.py')) ||
149
+ existsSync(join(dir, 'setup.cfg')) ||
150
+ existsSync(join(dir, 'requirements.txt')) ||
151
+ existsSync(join(dir, 'manage.py')));
152
+ }
153
+ /** Immediate subdirs (depth 1) worth a nested Django scan (sorted → deterministic). */
154
+ function shallowScanSubdirs(base) {
155
+ let entries;
156
+ try {
157
+ entries = readdirSync(base, { withFileTypes: true });
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ const out = [];
163
+ for (const e of entries) {
164
+ if (!e.isDirectory() || e.name.startsWith('.') || SKIP_DIRS.has(e.name))
165
+ continue;
166
+ if (hasDjangoScanTarget(join(base, e.name)))
167
+ out.push(e.name);
168
+ }
169
+ return out.sort();
170
+ }
171
+ /**
172
+ * Decide Django from the signal set. `django` (dep) is the authoritative signal;
173
+ * absent it, a `manage.py` + a settings module is Django-unique enough to claim at
174
+ * lower confidence. DRF / Ninja only ride in metadata (they enable their passes,
175
+ * they don't decide the stack). Returns null → generic-Python fallthrough,
176
+ * byte-for-byte unchanged.
177
+ */
178
+ export function scoreDjango(s, rootPath = '') {
179
+ let confidence = 0;
180
+ if (s.hasDjango) {
181
+ confidence = 0.85;
182
+ if (s.hasManagePy)
183
+ confidence += 0.05;
184
+ if (s.hasDrf || s.hasNinja)
185
+ confidence += 0.05;
186
+ }
187
+ else if (s.hasManagePy && s.hasSettings) {
188
+ // No declared dep (vendored / undeclared django), but the launcher + settings
189
+ // module are Django-specific — a real, lower-confidence match.
190
+ confidence = 0.7;
191
+ }
192
+ else {
193
+ return null;
194
+ }
195
+ return {
196
+ adapter: 'django',
197
+ confidence: clampConfidence(confidence),
198
+ rootPath,
199
+ metadata: {
200
+ drf: s.hasDrf,
201
+ ninja: s.hasNinja,
202
+ signals: {
203
+ django: s.hasDjango,
204
+ drf: s.hasDrf,
205
+ ninja: s.hasNinja,
206
+ managePy: s.hasManagePy,
207
+ settings: s.hasSettings,
208
+ },
209
+ },
210
+ };
211
+ }
212
+ // Collapse priority when several files of different roles land in ONE module after
213
+ // clustering (the contribute-step keeps the highest). Request entries (views)
214
+ // outrank jobs outrank data modules outrank app wiring.
215
+ const ROLE_PRIORITY = {
216
+ view: 7,
217
+ command: 6,
218
+ model: 5,
219
+ 'app-config': 4,
220
+ };
221
+ const ROLE_KIND = {
222
+ view: 'gateway',
223
+ command: 'job',
224
+ model: 'service',
225
+ 'app-config': 'service',
226
+ };
227
+ // URLconf entry callables (`path(route, view)` / `re_path(pat, view)` / legacy
228
+ // `url(pat, view)`), the sub-URLconf mount (`include(...)`), and the HTTP methods a
229
+ // Django-Ninja router/api exposes as decorators (`@router.get`, `@api.post`, …).
230
+ const URLCONF_FNS = new Set(['path', 're_path', 'url']);
231
+ const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']);
232
+ // Constructor last-segments (`x = Ctor(...)`) that mark a first-party object:
233
+ // DRF routers register viewsets; Ninja NinjaAPI/Router declare route surfaces;
234
+ // a Signal() is a first-party pub/sub channel.
235
+ const DRF_ROUTER_CTORS = new Set(['DefaultRouter', 'SimpleRouter']);
236
+ const NINJA_CTORS = new Set(['NinjaAPI', 'Router']);
237
+ const SIGNAL_CTOR = 'Signal';
238
+ // Model field constructors whose FIRST arg is the related model (the relationship
239
+ // edge). `models.ForeignKey(Author)` / `OneToOneField('Profile')` / `ManyToManyField('app.Tag')`.
240
+ const RELATION_FIELDS = {
241
+ ForeignKey: 'model-fk',
242
+ OneToOneField: 'model-o2o',
243
+ ManyToManyField: 'model-m2m',
244
+ };
245
+ // Base-class last-segments that identify a class stereotype (matched on the LAST
246
+ // chain segment, so `models.Model`, `Model`, and `django.db.models.Model` all read
247
+ // as 'Model'). Views are matched by the /(View|ViewSet)$/ naming convention, which
248
+ // covers APIView / ModelViewSet / ListView / custom *View bases + their subclasses.
249
+ const MODEL_BASES = new Set(['Model', 'AbstractUser', 'AbstractBaseUser']);
250
+ const COMMAND_BASES = new Set(['BaseCommand', 'AppCommand', 'LabelCommand']);
251
+ const APPCONFIG_BASE = 'AppConfig';
252
+ const VIEW_BASE_RE = /(?:View|ViewSet)$/;
253
+ // Function decorators that mark a plain function as a request entry (DRF's
254
+ // `@api_view([...])`).
255
+ const FN_VIEW_DECORATORS = new Set(['api_view']);
256
+ // ---------------------------------------------------------------------------
257
+ // Local class collection. The shared py-ast `collectNodes` gathers assignments /
258
+ // functions / calls / imports (recursive) but NOT class nodes — FastAPI didn't
259
+ // need them. Django's roles/grouping/edges hinge on class BASES (models.Model,
260
+ // AppConfig, BaseCommand, *ViewSet) + per-class body fields, so we collect
261
+ // ClassNodes with a small LOCAL walker (kept local so the shared py-ast surface is
262
+ // untouched — zero coupling with the parallel Python-adapter fan-out). Pyright's
263
+ // visitClass drives collection; the only pinned value we branch on is the
264
+ // StatementList node-type (47), for one-level class-body unwrapping.
265
+ const NODE_STATEMENT_LIST = 47;
266
+ const NODE_TUPLE = 52;
267
+ class ClassCollector extends ParseTreeWalker {
268
+ out = [];
269
+ visitClass(node) {
270
+ this.out.push(node);
271
+ return true; // descend — nested classes matter (an inner Meta/AppConfig)
272
+ }
273
+ }
274
+ function collectClasses(tree) {
275
+ const c = new ClassCollector();
276
+ c.walk(tree);
277
+ return c.out;
278
+ }
279
+ /** A class's declared base-class chains (positional args only; kwargs like
280
+ * `metaclass=` are skipped). Each entry is the memberChain of a base expr. */
281
+ function classBases(cls) {
282
+ const out = [];
283
+ for (const arg of cls.d.arguments) {
284
+ if (arg.d.name)
285
+ continue; // keyword arg (metaclass=…), not a base
286
+ const chain = memberChain(arg.d.valueExpr);
287
+ if (!chain)
288
+ continue;
289
+ const last = chain.path.length ? chain.path[chain.path.length - 1] : chain.root;
290
+ out.push({ root: chain.root, last, dotted: chain.path.length > 0 });
291
+ }
292
+ return out;
293
+ }
294
+ /** A class's DIRECT body assignments (one level — model fields / AppConfig attrs),
295
+ * NOT method-body assignments. Simple statements are wrapped in a StatementList. */
296
+ function classBodyAssignments(cls) {
297
+ const out = [];
298
+ for (const stmt of cls.d.suite.d.statements) {
299
+ if (stmt.nodeType !== NODE_STATEMENT_LIST)
300
+ continue;
301
+ for (const inner of stmt.d.statements) {
302
+ if (inner.nodeType === PN.Assignment)
303
+ out.push(inner);
304
+ }
305
+ }
306
+ return out;
307
+ }
308
+ function className(cls) {
309
+ return cls.d.name.d.value;
310
+ }
311
+ // ---------------------------------------------------------------------------
312
+ // String / name helpers (mirror the FastAPI adapter's local set).
313
+ function slugify(s) {
314
+ return s
315
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
316
+ .toLowerCase()
317
+ .replace(/[^a-z0-9]+/g, '-')
318
+ .replace(/^-+|-+$/g, '');
319
+ }
320
+ function humanize(s) {
321
+ const words = s
322
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
323
+ .replace(/[_\-.]+/g, ' ')
324
+ .trim()
325
+ .split(/\s+/)
326
+ .filter(Boolean);
327
+ if (words.length === 0)
328
+ return s;
329
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
330
+ }
331
+ function baseName(fileOrDir) {
332
+ const last = fileOrDir.split('/').pop() ?? fileOrDir;
333
+ return last.replace(/\.[^.]+$/, '');
334
+ }
335
+ function dirSegment(fileId) {
336
+ const slash = fileId.indexOf('/');
337
+ return slash > 0 ? slugify(fileId.slice(0, slash)) : 'root';
338
+ }
339
+ function isViewsFile(fileId) {
340
+ // views.py, or any file under a `views/` package (`app/views/users.py`).
341
+ const parts = fileId.split('/');
342
+ if (baseName(fileId) === 'views')
343
+ return true;
344
+ return parts.slice(0, -1).includes('views');
345
+ }
346
+ // The constructor name of an `x = Ctor(...)` RHS (last chain segment), or undefined.
347
+ function assignedCtorLast(rhs) {
348
+ if (rhs.nodeType !== PN.Call)
349
+ return undefined;
350
+ const callee = callCallee(rhs);
351
+ if (!callee)
352
+ return undefined;
353
+ return callee.path.length ? callee.path[callee.path.length - 1] : callee.root;
354
+ }
355
+ // A decorator's callee chain (`@router.get(...)` → root 'router', path ['get'];
356
+ // `@receiver(...)` → root 'receiver', path []; `@api_view([...])` → 'api_view').
357
+ function decoratorChain(deco) {
358
+ const expr = deco.d.expr;
359
+ if (expr.nodeType === PN.Call)
360
+ return callCallee(expr);
361
+ return memberChain(expr);
362
+ }
363
+ // The Call node of a decorator, IF it is a call (`@receiver(sig, sender=X)`).
364
+ function decoratorCall(deco) {
365
+ const expr = deco.d.expr;
366
+ return expr.nodeType === PN.Call ? expr : undefined;
367
+ }
368
+ // The first item of a tuple expression, or undefined. Django's `include(('app.urls',
369
+ // 'namespace'))` app-namespacing form wraps the module in a 2-tuple; we route on
370
+ // its first element.
371
+ function firstTupleItem(expr) {
372
+ if (!expr || expr.nodeType !== NODE_TUPLE)
373
+ return undefined;
374
+ return expr.d.items[0];
375
+ }
376
+ // The root identifier of a reference expression: a Name / MemberAccess uses its
377
+ // chain root (`views.foo` → 'views'); a Call uses its callee root (`Foo.as_view()`
378
+ // → 'Foo', `include('x')` → 'include'). Undefined for anything else.
379
+ function refRoot(expr) {
380
+ if (!expr)
381
+ return undefined;
382
+ if (expr.nodeType === PN.Call)
383
+ return callCallee(expr)?.root;
384
+ return memberChain(expr)?.root;
385
+ }
386
+ // Memoized on the FrameworkContext OBJECT (not repoDir) so groupingPrior +
387
+ // syntheticEdges + roleTags share ONE parse, while the merge walk's per-checkpoint
388
+ // ctx gets a fresh analysis — no cross-tree staleness. Mirrors fastapi / nest / RN.
389
+ const ANALYSIS_CACHE = new WeakMap();
390
+ function addRole(map, fileId, role) {
391
+ const cur = map.get(fileId);
392
+ if (cur === undefined ||
393
+ ROLE_PRIORITY[role] > ROLE_PRIORITY[cur] ||
394
+ (ROLE_PRIORITY[role] === ROLE_PRIORITY[cur] && role < cur)) {
395
+ map.set(fileId, role);
396
+ }
397
+ }
398
+ function addEdge(edges, from, to, kind, relation) {
399
+ if (from === to)
400
+ return; // intra-file wiring collapses; the step drops self-edges too
401
+ const key = `${from}→${to}:${kind}`;
402
+ if (!edges.has(key)) {
403
+ edges.set(key, { source: from, target: to, kind, metadata: { framework: 'django', relation } });
404
+ }
405
+ }
406
+ // Parse every in-scope Python file once, keeping the tree long enough to collect
407
+ // classes (which the shared parsePythonScope's CollectedNodes doesn't expose). We
408
+ // reuse the shared atomics (collectNodes / buildImportBindings / inferSourceRoots /
409
+ // isPythonFile / inScope) — same single-parse cost as parsePythonScope, plus classes.
410
+ function parseDjangoScope(ctx) {
411
+ const { repoDir, rootPath, graph } = ctx;
412
+ const pyFiles = graph.files
413
+ .filter((f) => isPythonFile(f.language) && inScope(f.id, rootPath))
414
+ .map((f) => f.id);
415
+ const internalIds = new Set(pyFiles);
416
+ const roots = inferSourceRoots(internalIds);
417
+ const parsed = new Map();
418
+ for (const id of pyFiles) {
419
+ let text;
420
+ try {
421
+ text = readFileSync(join(repoDir, id), 'utf8');
422
+ }
423
+ catch {
424
+ continue;
425
+ }
426
+ const tree = parsePython(text);
427
+ if (!tree)
428
+ continue;
429
+ const nodes = collectNodes(tree);
430
+ parsed.set(id, {
431
+ nodes,
432
+ classes: collectClasses(tree),
433
+ bindings: buildImportBindings(id, nodes.imports, internalIds, roots),
434
+ });
435
+ }
436
+ return { pyFiles, internalIds, roots, parsed };
437
+ }
438
+ function classKey(fileId, name) {
439
+ return `${fileId}#${name}`;
440
+ }
441
+ // Resolve a base-class chain to the class key it refers to, or undefined.
442
+ // * bare `Base` → imported file (binds) / a local class in this file
443
+ // * dotted `mod.Base`→ the module alias's file (binds[root]); class name = last
444
+ function resolveBaseClass(base, info) {
445
+ const targetFile = base.dotted
446
+ ? info.binds.get(base.root)
447
+ : (info.binds.get(base.root) ?? (info.localClasses.has(base.root) ? info.fileId : undefined));
448
+ if (!targetFile)
449
+ return undefined;
450
+ return classKey(targetFile, base.last);
451
+ }
452
+ function computeModelClasses(classes) {
453
+ const isModel = new Set();
454
+ // Seed: direct Django-model bases.
455
+ for (const c of classes) {
456
+ if (c.bases.some((b) => MODEL_BASES.has(b.last)))
457
+ isModel.add(classKey(c.fileId, c.name));
458
+ }
459
+ // Fixpoint: a class whose base resolves to a known model class is a model too.
460
+ let changed = true;
461
+ let guard = 0;
462
+ while (changed && guard++ < classes.length + 1) {
463
+ changed = false;
464
+ for (const c of classes) {
465
+ const key = classKey(c.fileId, c.name);
466
+ if (isModel.has(key))
467
+ continue;
468
+ for (const b of c.bases) {
469
+ const resolved = resolveBaseClass(b, c);
470
+ if (resolved && isModel.has(resolved)) {
471
+ isModel.add(key);
472
+ changed = true;
473
+ break;
474
+ }
475
+ }
476
+ }
477
+ }
478
+ return isModel;
479
+ }
480
+ function appMarkerDirs(pyFileSet) {
481
+ const dirs = new Set();
482
+ for (const id of pyFileSet) {
483
+ let appDir;
484
+ if (id.endsWith('/apps.py'))
485
+ appDir = id.slice(0, -'/apps.py'.length);
486
+ else if (id.endsWith('/models.py'))
487
+ appDir = id.slice(0, -'/models.py'.length);
488
+ else if (id.endsWith('/models/__init__.py'))
489
+ appDir = id.slice(0, -'/models/__init__.py'.length);
490
+ if (appDir !== undefined && appDir !== '')
491
+ dirs.add(appDir);
492
+ }
493
+ return dirs;
494
+ }
495
+ // The verbose_name declared on the app's AppConfig (a plain string literal), or
496
+ // undefined (gettext-lazy `_(...)` and non-literals fall back to the dir name).
497
+ function appConfigVerboseName(parsed) {
498
+ if (!parsed)
499
+ return undefined;
500
+ for (const cls of parsed.classes) {
501
+ if (!classBases(cls).some((b) => b.last === APPCONFIG_BASE))
502
+ continue;
503
+ for (const a of classBodyAssignments(cls)) {
504
+ if (nameValue(a.d.leftExpr) !== 'verbose_name')
505
+ continue;
506
+ const v = stringValue(a.d.rightExpr);
507
+ if (v && v.trim())
508
+ return v.trim();
509
+ }
510
+ }
511
+ return undefined;
512
+ }
513
+ function assignAppGroupIds(seeds, fileIdsByDir) {
514
+ const taken = new Set();
515
+ const byDir = [...seeds].sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
516
+ const groups = [];
517
+ for (const seed of byDir) {
518
+ let id = seed.baseSlug;
519
+ if (taken.has(id))
520
+ id = `${seed.baseSlug}-${dirSegment(seed.dir)}`;
521
+ let n = 2;
522
+ while (taken.has(id))
523
+ id = `${seed.baseSlug}-${n++}`;
524
+ taken.add(id);
525
+ groups.push({ id, label: seed.label, fileIds: (fileIdsByDir.get(seed.dir) ?? []).slice().sort() });
526
+ }
527
+ return groups.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
528
+ }
529
+ function buildAppGroups(pyFiles, pyFileSet, parsed) {
530
+ const appDirs = [...appMarkerDirs(pyFileSet)].sort((a, b) => b.length - a.length); // deepest first
531
+ if (appDirs.length === 0)
532
+ return [];
533
+ const fileIdsByDir = new Map();
534
+ for (const id of pyFiles) {
535
+ const app = appDirs.find((d) => id === d || id.startsWith(`${d}/`));
536
+ if (!app)
537
+ continue;
538
+ (fileIdsByDir.get(app) ?? fileIdsByDir.set(app, []).get(app)).push(id);
539
+ }
540
+ const seeds = [];
541
+ for (const dir of fileIdsByDir.keys()) {
542
+ const verbose = appConfigVerboseName(parsed.get(`${dir}/apps.py`));
543
+ const label = verbose ?? humanize(baseName(dir));
544
+ seeds.push({ dir, baseSlug: slugify(baseName(dir)) || 'app', label });
545
+ }
546
+ return assignAppGroupIds(seeds, fileIdsByDir);
547
+ }
548
+ // ── The full analysis ─────────────────────────────────────────────────────
549
+ function analyzeDjango(ctx) {
550
+ const { parsed, internalIds, roots } = parseDjangoScope(ctx);
551
+ const pyFiles = [...parsed.keys()];
552
+ const pyFileSet = internalIds;
553
+ const roleByFile = new Map();
554
+ const edges = new Map();
555
+ const diag = {
556
+ unresolvedViews: new Set(),
557
+ unresolvedIncludes: new Set(),
558
+ unresolvedRelations: new Set(),
559
+ ambiguousRelations: new Set(),
560
+ };
561
+ // Pass 1 — per-file object vars (DRF routers, Ninja apis, Signals) + a global
562
+ // class index (for transitive model detection + string-FK resolution).
563
+ const drfRouterVarsByFile = new Map();
564
+ const ninjaVarsByFile = new Map();
565
+ const signalVarsByFile = new Map();
566
+ const localClassNamesByFile = new Map();
567
+ const classInfos = [];
568
+ for (const [id, pf] of parsed) {
569
+ const drfRouters = new Set();
570
+ const ninjas = new Set();
571
+ const signals = new Set();
572
+ for (const a of pf.nodes.assignments) {
573
+ const target = nameValue(a.d.leftExpr);
574
+ if (!target)
575
+ continue;
576
+ const ctor = assignedCtorLast(a.d.rightExpr);
577
+ if (!ctor)
578
+ continue;
579
+ if (DRF_ROUTER_CTORS.has(ctor))
580
+ drfRouters.add(target);
581
+ else if (NINJA_CTORS.has(ctor))
582
+ ninjas.add(target);
583
+ else if (ctor === SIGNAL_CTOR)
584
+ signals.add(target);
585
+ }
586
+ drfRouterVarsByFile.set(id, drfRouters);
587
+ ninjaVarsByFile.set(id, ninjas);
588
+ signalVarsByFile.set(id, signals);
589
+ const localNames = new Set();
590
+ for (const cls of pf.classes)
591
+ localNames.add(className(cls));
592
+ localClassNamesByFile.set(id, localNames);
593
+ }
594
+ for (const [id, pf] of parsed) {
595
+ const localNames = localClassNamesByFile.get(id);
596
+ for (const cls of pf.classes) {
597
+ classInfos.push({
598
+ fileId: id,
599
+ name: className(cls),
600
+ bases: classBases(cls),
601
+ binds: pf.bindings,
602
+ localClasses: localNames,
603
+ });
604
+ }
605
+ }
606
+ const modelClasses = computeModelClasses(classInfos);
607
+ // Model index: className → sorted files that define a model class of that name
608
+ // (for string-target FK resolution: `ForeignKey('Author')`).
609
+ const modelFilesByName = new Map();
610
+ const modelFiles = new Set();
611
+ for (const info of classInfos) {
612
+ if (!modelClasses.has(classKey(info.fileId, info.name)))
613
+ continue;
614
+ modelFiles.add(info.fileId);
615
+ const list = modelFilesByName.get(info.name) ?? [];
616
+ if (!list.includes(info.fileId))
617
+ list.push(info.fileId);
618
+ modelFilesByName.set(info.name, list.sort());
619
+ }
620
+ // Pass 2 — roles, edges.
621
+ for (const [id, pf] of parsed) {
622
+ const binds = pf.bindings;
623
+ const localClasses = localClassNamesByFile.get(id);
624
+ const drfRouters = drfRouterVarsByFile.get(id);
625
+ const ninjas = ninjaVarsByFile.get(id);
626
+ const localSignals = signalVarsByFile.get(id);
627
+ // Resolve a view/router/model reference expr → target file (import binding, or
628
+ // a co-located local class).
629
+ const resolveRef = (expr) => {
630
+ const root = refRoot(expr);
631
+ if (!root)
632
+ return undefined;
633
+ return binds.get(root) ?? (localClasses.has(root) ? id : undefined);
634
+ };
635
+ // Resolve a first-party Signal reference (imported or same-file) → its file.
636
+ const resolveSignal = (expr) => {
637
+ const root = refRoot(expr);
638
+ if (!root)
639
+ return undefined;
640
+ const viaImport = binds.get(root);
641
+ if (viaImport && signalVarsByFile.get(viaImport)?.size)
642
+ return viaImport;
643
+ if (localSignals.has(root))
644
+ return id;
645
+ return undefined;
646
+ };
647
+ // ── Roles from classes ──
648
+ const fileHasFunctionOrClass = pf.nodes.functions.length > 0 || pf.classes.length > 0;
649
+ if (modelFiles.has(id))
650
+ addRole(roleByFile, id, 'model');
651
+ for (const cls of pf.classes) {
652
+ const bases = classBases(cls);
653
+ const lasts = bases.map((b) => b.last);
654
+ if (lasts.some((l) => COMMAND_BASES.has(l)))
655
+ addRole(roleByFile, id, 'command');
656
+ if (lasts.some((l) => l === APPCONFIG_BASE))
657
+ addRole(roleByFile, id, 'app-config');
658
+ // Class-based views + DRF ViewSets: base name ends with View/ViewSet.
659
+ if (lasts.some((l) => VIEW_BASE_RE.test(l)))
660
+ addRole(roleByFile, id, 'view');
661
+ }
662
+ // ── Roles from functions (Ninja ops / DRF @api_view) + views.py convention ──
663
+ for (const fn of pf.nodes.functions) {
664
+ for (const deco of fn.d.decorators) {
665
+ const chain = decoratorChain(deco);
666
+ if (!chain)
667
+ continue;
668
+ // @<ninjaApi|router>.<httpmethod>(...) → a Ninja route handler.
669
+ if (chain.path.length === 1 && HTTP_METHODS.has(chain.path[0]) && ninjas.has(chain.root)) {
670
+ addRole(roleByFile, id, 'view');
671
+ }
672
+ // @api_view([...]) → a DRF function-based view.
673
+ if (chain.path.length === 0 && FN_VIEW_DECORATORS.has(chain.root)) {
674
+ addRole(roleByFile, id, 'view');
675
+ }
676
+ }
677
+ }
678
+ // The Django convention: a views.py / views/ package module is the request
679
+ // entry (gateway), even for plain function-based views the parser can't
680
+ // fingerprint. Require some content to avoid tagging an empty file.
681
+ if (isViewsFile(id) && fileHasFunctionOrClass)
682
+ addRole(roleByFile, id, 'view');
683
+ // ── Model relationship edges (FK / OneToOne / ManyToMany) ──
684
+ for (const cls of pf.classes) {
685
+ if (!modelClasses.has(classKey(id, className(cls))))
686
+ continue;
687
+ for (const a of classBodyAssignments(cls)) {
688
+ if (!nameValue(a.d.leftExpr))
689
+ continue; // a plain `field = …` assignment
690
+ const rhs = a.d.rightExpr;
691
+ if (rhs.nodeType !== PN.Call)
692
+ continue;
693
+ const ctorLast = assignedCtorLast(rhs);
694
+ if (!ctorLast || !(ctorLast in RELATION_FIELDS))
695
+ continue;
696
+ const relation = RELATION_FIELDS[ctorLast];
697
+ const call = rhs;
698
+ const targetArg = positionalArgs(call)[0] ?? keywordArg(call, 'to');
699
+ const target = resolveRelationTarget(targetArg, id, binds, localClasses, modelFilesByName, diag);
700
+ if (target)
701
+ addEdge(edges, id, target, 'calls', relation);
702
+ else if (targetArg)
703
+ diag.unresolvedRelations.add(`${id}: ${ctorLast}(…)`);
704
+ }
705
+ }
706
+ // ── URLconf + include() + DRF register + Ninja add_router edges ──
707
+ for (const call of pf.nodes.calls) {
708
+ const callee = callCallee(call);
709
+ if (!callee)
710
+ continue;
711
+ // path(route, view) / re_path(pat, view) / url(pat, view) — a bare-name call.
712
+ if (callee.path.length === 0 && URLCONF_FNS.has(callee.root)) {
713
+ const viewArg = positionalArgs(call)[1] ?? keywordArg(call, 'view');
714
+ // `include(...)` nested as the view is handled by the include scan below.
715
+ if (viewArg && refRoot(viewArg) === 'include')
716
+ continue;
717
+ const target = resolveRef(viewArg);
718
+ if (target)
719
+ addEdge(edges, id, target, 'calls', 'url-route');
720
+ else if (viewArg)
721
+ diag.unresolvedViews.add(`${id}: ${callee.root}(…)`);
722
+ continue;
723
+ }
724
+ // include('app.urls') / include(module) / include(('app.urls', 'ns')) — a
725
+ // sub-URLconf mount. The tuple form (app-namespacing) wraps the module first.
726
+ if (callee.path.length === 0 && callee.root === 'include') {
727
+ const rawArg = positionalArgs(call)[0];
728
+ const arg = firstTupleItem(rawArg) ?? rawArg;
729
+ const str = arg ? stringValue(arg) : undefined;
730
+ if (str) {
731
+ const target = resolveDottedModule(str, id, internalIds, roots);
732
+ if (target)
733
+ addEdge(edges, id, target, 'calls', 'url-include');
734
+ else
735
+ diag.unresolvedIncludes.add(`${id}: include('${str}')`);
736
+ }
737
+ else {
738
+ const target = resolveRef(arg);
739
+ if (target)
740
+ addEdge(edges, id, target, 'calls', 'url-include');
741
+ else
742
+ diag.unresolvedIncludes.add(`${id}: include(…)`);
743
+ }
744
+ continue;
745
+ }
746
+ if (callee.path.length !== 1)
747
+ continue; // the remaining edges are `obj.method(...)`
748
+ const method = callee.path[0];
749
+ const obj = callee.root;
750
+ // <drfRouter>.register(prefix, ViewSet, …) → routes to the viewset.
751
+ if (method === 'register' && drfRouters.has(obj)) {
752
+ const viewSetArg = positionalArgs(call)[1] ?? keywordArg(call, 'viewset');
753
+ const target = resolveRef(viewSetArg);
754
+ if (target)
755
+ addEdge(edges, id, target, 'calls', 'drf-register');
756
+ else if (viewSetArg)
757
+ diag.unresolvedViews.add(`${id}: ${obj}.register(…)`);
758
+ continue;
759
+ }
760
+ // <ninjaApi>.add_router(prefix, router) → mounts a sub-router.
761
+ if (method === 'add_router' && ninjas.has(obj)) {
762
+ const routerArg = positionalArgs(call)[1] ?? keywordArg(call, 'router');
763
+ const target = resolveRef(routerArg);
764
+ if (target)
765
+ addEdge(edges, id, target, 'calls', 'ninja-add-router');
766
+ else if (routerArg)
767
+ diag.unresolvedViews.add(`${id}: ${obj}.add_router(…)`);
768
+ continue;
769
+ }
770
+ // <signal>.send(...) / .send_robust(...) → publishes; .connect(...) → subscribes.
771
+ if (method === 'send' || method === 'send_robust' || method === 'connect') {
772
+ const sigFile = resolveSignal(memberBaseExpr(call));
773
+ if (sigFile) {
774
+ if (method === 'connect')
775
+ addEdge(edges, id, sigFile, 'subscribes', 'signal-connect');
776
+ else
777
+ addEdge(edges, id, sigFile, 'publishes', 'signal-send');
778
+ }
779
+ continue;
780
+ }
781
+ }
782
+ // ── @receiver(signal, sender=Model) decorators → pub/sub edges ──
783
+ for (const fn of pf.nodes.functions) {
784
+ for (const deco of fn.d.decorators) {
785
+ const chain = decoratorChain(deco);
786
+ if (!chain || chain.path.length !== 0 || chain.root !== 'receiver')
787
+ continue;
788
+ const call = decoratorCall(deco);
789
+ if (!call)
790
+ continue;
791
+ // receiver subscribes to a first-party Signal.
792
+ const sigTarget = resolveSignal(positionalArgs(call)[0] ?? keywordArg(call, 'signal'));
793
+ if (sigTarget)
794
+ addEdge(edges, id, sigTarget, 'subscribes', 'signal-receiver');
795
+ // sender=Model → the model publishes its lifecycle event to this receiver.
796
+ const senderArg = keywordArg(call, 'sender');
797
+ const senderFile = resolveRef(senderArg);
798
+ if (senderFile && modelFiles.has(senderFile)) {
799
+ addEdge(edges, senderFile, id, 'publishes', 'signal-sender');
800
+ }
801
+ }
802
+ }
803
+ }
804
+ const groups = buildAppGroups(pyFiles, pyFileSet, parsed);
805
+ const roles = new Map();
806
+ for (const [fileId, role] of roleByFile) {
807
+ roles.set(fileId, {
808
+ role,
809
+ kind: ROLE_KIND[role],
810
+ priority: ROLE_PRIORITY[role],
811
+ metadata: { framework: 'django' },
812
+ });
813
+ }
814
+ const sortedEdges = [...edges.values()].sort((a, b) => a.source < b.source
815
+ ? -1
816
+ : a.source > b.source
817
+ ? 1
818
+ : a.target < b.target
819
+ ? -1
820
+ : a.target > b.target
821
+ ? 1
822
+ : a.kind < b.kind
823
+ ? -1
824
+ : a.kind > b.kind
825
+ ? 1
826
+ : 0);
827
+ // Positive signal for validation (mirrors fastapi/nest).
828
+ if (groups.length > 0 || roleByFile.size > 0 || sortedEdges.length > 0) {
829
+ console.log(` [django] ${groups.length} app group(s) · ${roleByFile.size} role(s) · ${sortedEdges.length} edge(s)`);
830
+ }
831
+ // No silent caps (locked): log everything that degraded.
832
+ const degraded = [];
833
+ if (diag.unresolvedViews.size > 0)
834
+ degraded.push(`${diag.unresolvedViews.size} unresolvable view(s): ${[...diag.unresolvedViews].sort().slice(0, 10).join(' · ')}`);
835
+ if (diag.unresolvedIncludes.size > 0)
836
+ degraded.push(`${diag.unresolvedIncludes.size} unresolvable include(s): ${[...diag.unresolvedIncludes].sort().slice(0, 10).join(' · ')}`);
837
+ if (diag.unresolvedRelations.size > 0)
838
+ degraded.push(`${diag.unresolvedRelations.size} unresolvable relation(s): ${[...diag.unresolvedRelations].sort().slice(0, 10).join(' · ')}`);
839
+ if (diag.ambiguousRelations.size > 0)
840
+ degraded.push(`${diag.ambiguousRelations.size} ambiguous string-model relation(s): ${[...diag.ambiguousRelations].sort().slice(0, 10).join(' · ')}`);
841
+ if (degraded.length > 0) {
842
+ console.log(` [django] degraded: ${degraded.join(' · ')} (logged, not silently dropped)`);
843
+ }
844
+ return { groups, edges: sortedEdges, roles };
845
+ }
846
+ // The base expression of an `obj.method(...)` call (the receiver `obj`) — used to
847
+ // resolve a `<signal>.send()` / `.connect()` receiver back to its Signal file.
848
+ function memberBaseExpr(call) {
849
+ const leftExpr = call.d.leftExpr;
850
+ if (leftExpr.nodeType !== PN.MemberAccess)
851
+ return undefined;
852
+ return leftExpr.d.leftExpr;
853
+ }
854
+ // Resolve a `path`/`re_path` `include('dotted.module')` string to a first-party
855
+ // module file via the extractor's path-anchored resolver (`app.urls` → app/urls.py).
856
+ function resolveDottedModule(dotted, fromId, internalIds, roots) {
857
+ const clean = dotted.trim();
858
+ if (!clean || clean.includes('://'))
859
+ return undefined;
860
+ return syntacticResolve(clean, fromId, internalIds, roots);
861
+ }
862
+ // Resolve a model-relationship target (`ForeignKey(Author)` / `('Author')` /
863
+ // `('app.Author')` / `('self')`) to the model's file.
864
+ function resolveRelationTarget(arg, fromId, binds, localClasses, modelFilesByName, diag) {
865
+ if (!arg)
866
+ return undefined;
867
+ // String target: 'self' | 'ModelName' | 'app_label.ModelName'.
868
+ const str = stringValue(arg);
869
+ if (str !== undefined) {
870
+ if (str === 'self' || str === '')
871
+ return undefined; // self-reference → no edge
872
+ const modelName = str.includes('.') ? str.split('.').pop() : str;
873
+ const files = modelFilesByName.get(modelName);
874
+ if (!files || files.length === 0)
875
+ return undefined;
876
+ if (files.length > 1)
877
+ diag.ambiguousRelations.add(`'${str}'→${files.join(',')}`);
878
+ return files[0]; // sorted → deterministic pick on ambiguity
879
+ }
880
+ // Class reference: `Author` (imported / local) or `pkg.Author`.
881
+ const root = refRoot(arg);
882
+ if (!root)
883
+ return undefined;
884
+ return binds.get(root) ?? (localClasses.has(root) ? fromId : undefined);
885
+ }
886
+ function getAnalysis(ctx) {
887
+ let a = ANALYSIS_CACHE.get(ctx);
888
+ if (!a) {
889
+ a = analyzeDjango(ctx);
890
+ ANALYSIS_CACHE.set(ctx, a);
891
+ }
892
+ return a;
893
+ }
894
+ // ---------------------------------------------------------------------------
895
+ // The adapter.
896
+ export const djangoAdapter = {
897
+ name: 'django',
898
+ async detect(ctx) {
899
+ const { base, rootPath } = resolveBase(ctx);
900
+ // Root scan first (a repo whose manifest/manage.py is at root → rootPath '').
901
+ const rootMatch = scoreDjango(gatherDjangoSignals(base), rootPath);
902
+ if (rootMatch)
903
+ return rootMatch;
904
+ // Nested backend (`backend/` | `server/` | `src/` Django package) — a shallow
905
+ // scan of immediate subdirs, scoping the adapter to the first match. Only when
906
+ // NOT already scoped to a workspace package (that's the per-package fan-out).
907
+ if (!ctx.packageDir) {
908
+ for (const sub of shallowScanSubdirs(base)) {
909
+ const m = scoreDjango(gatherDjangoSignals(join(base, sub)), sub);
910
+ if (m)
911
+ return m;
912
+ }
913
+ }
914
+ return null;
915
+ },
916
+ // One grouping prior per Django app → its own subsystem, authoritative over
917
+ // directory grouping (the Nest/RN mechanism). Fully deterministic (path/name-
918
+ // derived) → no classificationsNeeded.
919
+ async groupingPrior(ctx) {
920
+ return { groups: getAnalysis(ctx).groups };
921
+ },
922
+ // URLconf / include / FK-O2O-M2M / DRF-register / Ninja-add-router / signals.
923
+ // File-id endpoints; the step resolves to modules, drops self-edges, dedupes,
924
+ // 8-verb-validates.
925
+ async syntheticEdges(ctx) {
926
+ return getAnalysis(ctx).edges;
927
+ },
928
+ // view/viewset/ninja-op → gateway; model module → service; Command → job;
929
+ // AppConfig → service. METADATA; the module's `kind` is unchanged.
930
+ async roleTags(ctx) {
931
+ return getAnalysis(ctx).roles;
932
+ },
933
+ // The hooks READ SOURCE (Python). Declare the paths the diff-driven hosted walk
934
+ // must treat as framework-relevant. Never-store-source holds: parse server-side,
935
+ // persist only the derived groups/edges/roles.
936
+ scansSourcePath(path) {
937
+ return path.endsWith('.py') || path.endsWith('.pyi');
938
+ },
939
+ };
940
+ //# sourceMappingURL=django.js.map