@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,674 @@
1
+ // Module clustering + god-node detection (/326).
2
+ //
3
+ // Turns the file-level NormalizedGraph into the diagram's MODULES. Boundaries
4
+ // come from Louvain community detection on the import/call graph — the
5
+ // deterministic, content-derived source. Explicitly NOT folders (a bad graph
6
+ // for the messy-repo ICP) and NOT commit co-change (dies under coarse AI-agent
7
+ // commits). The LLM names clusters later; it never sets boundaries.
8
+ //
9
+ // Externals are NOT clustered — each external dependency is its own leaf module
10
+ // (kind 'external'); only internal files participate in community detection.
11
+ //
12
+ // Stage C (decides manifest-first, Louvain fallback): when a
13
+ // WorkspaceLayout with ≥2 packages is supplied, declared package boundaries
14
+ // become first-class — Louvain runs only WITHIN each package (small graphs,
15
+ // stable partitions, optionally warm-reused via PackagePartitionCache), the
16
+ // boundary BETWEEN packages is the manifest's word and never statistical, and
17
+ // cross-package edges aggregate to module edges at full confidence. A bare
18
+ // `import x from '@org/pkg-b'` that the install-free extractor classified as
19
+ // EXTERNAL is remapped back onto pkg-b's entry module when pkg-b is a
20
+ // workspace sibling. `stabilizeModuleIds` warm-starts module identity from
21
+ // the prior extraction so the changelog join key + the LLM set-diff
22
+ // (enrich/diff.ts) survive re-clusters — id stability directly cuts LLM spend.
23
+ // With no options (or a single-package layout) behavior is byte-identical to
24
+ // pre-Stage-C.
25
+ import Graph from 'graphology';
26
+ import louvain from 'graphology-communities-louvain';
27
+ import { externalIdFor } from '../graph/types.js';
28
+ import { compileMatchers, slugify } from './overrides.js';
29
+ // God-node detection ( / , fixes ). The old fixed-degree
30
+ // threshold fired on ~half the modules of a small dense repo (4/8 on backthread),
31
+ // which makes the signal meaningless. Replaced with a statistical-outlier rule
32
+ // that scales with graph size: a module is a god-node iff it is BOTH an
33
+ // internal-degree outlier (z-score > 1.5σ above the mean) AND a routing
34
+ // chokepoint (betweenness centrality in the top quartile). AND, not OR —
35
+ // precision over recall, because a false "scary to refactor" flag destroys
36
+ // trust in the signal. Below MIN_MODULES the statistics are noise, so we never
37
+ // flag.
38
+ const GOD_NODE_Z_THRESHOLD = 1.5;
39
+ const GOD_NODE_MIN_MODULES = 5;
40
+ // Seeded PRNG (mulberry32). Louvain defaults to Math.random for node ordering,
41
+ // so a fixed seed makes communities — and the module ids derived from them —
42
+ // stable across runs. Required for idempotent re-sync and P4 snapshot diffing.
43
+ function mulberry32(seed) {
44
+ return () => {
45
+ seed = (seed + 0x6d2b79f5) | 0;
46
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
47
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
48
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
49
+ };
50
+ }
51
+ // Derive the dominant path segment of a community's files: the most common
52
+ // segment after an optional leading `src/` (single-segment paths fall back to
53
+ // the extension-less basename). Pure naming heuristic — the LLM relabels for
54
+ // display; the id built from this is the stable join key + override target.
55
+ function deriveSegment(fileIds) {
56
+ const counts = new Map();
57
+ for (const f of fileIds) {
58
+ const parts = f.replace(/^src\//, '').split('/');
59
+ const seg = parts.length > 1 ? parts[0] : (parts[0].replace(/\.[^.]+$/, '') || 'root');
60
+ counts.set(seg, (counts.get(seg) ?? 0) + 1);
61
+ }
62
+ let best = 'module';
63
+ let bestN = -1;
64
+ for (const [seg, n] of counts) {
65
+ if (n > bestN) {
66
+ best = seg;
67
+ bestN = n;
68
+ }
69
+ }
70
+ return best;
71
+ }
72
+ // Reserve a unique module id from a base: `-2`, `-3`, … suffixes on collision.
73
+ // The ONE dedup gate every id-producing path goes through (derived, slug-
74
+ // prefixed, stabilization reroutes), so ids are unique by construction.
75
+ function reserveId(base, used) {
76
+ let id = base;
77
+ let i = 2;
78
+ while (used.has(id))
79
+ id = `${base}-${i++}`;
80
+ used.add(id);
81
+ return id;
82
+ }
83
+ // Derive a provisional module id from a community's files (pre-Stage-C form,
84
+ // used for the whole-graph path + root-scope communities).
85
+ function deriveModuleId(fileIds, used) {
86
+ return reserveId(slugify(deriveSegment(fileIds)) || 'module', used);
87
+ }
88
+ export class PackagePartitionCache {
89
+ entries = new Map();
90
+ dirty = new Set();
91
+ /**
92
+ * Mark the packages owning these paths dirty — call with each checkpoint's
93
+ * diff paths BEFORE clustering that checkpoint. Conservative on purpose:
94
+ * any touched path (source or not) dirties its owning package; a non-source
95
+ * path costs at most one redundant re-cluster of a small subgraph.
96
+ */
97
+ invalidatePaths(paths, layout) {
98
+ for (const p of paths)
99
+ this.dirty.add(layout.packageOf(p).slug);
100
+ }
101
+ /**
102
+ * Drop everything — call on a full re-extract, a workspace-layout change
103
+ * (a diff touching isWorkspaceManifestPath), or an overrides change (slugs
104
+ * and forced membership both feed the stored partitions).
105
+ */
106
+ invalidateAll() {
107
+ this.entries.clear();
108
+ this.dirty.clear();
109
+ }
110
+ /**
111
+ * The cluster-layer read: a stored partition is reusable ONLY when the
112
+ * package is clean AND its member set is unchanged (`memberKey` = sorted
113
+ * '\n'-join of member fileIds — the same key clusterGraph computes).
114
+ */
115
+ lookup(slug, memberKey) {
116
+ if (this.dirty.has(slug))
117
+ return null;
118
+ const e = this.entries.get(slug);
119
+ return e !== undefined && e.memberKey === memberKey ? e.communities : null;
120
+ }
121
+ /** The cluster-layer write after a (re-)cluster; clears the dirty mark. */
122
+ store(slug, memberKey, communities) {
123
+ this.entries.set(slug, { memberKey, communities });
124
+ this.dirty.delete(slug);
125
+ }
126
+ }
127
+ export function clusterGraph(graph, overrides, opts) {
128
+ const drop = compileMatchers(overrides?.drop ?? []);
129
+ const assign = (overrides?.assign ?? []).map((rule) => ({
130
+ match: compileMatchers([rule.pattern]),
131
+ moduleId: rule.moduleId,
132
+ }));
133
+ // Internal files minus dropped noise.
134
+ const files = graph.files.filter((f) => !drop(f.id));
135
+ const fileSet = new Set(files.map((f) => f.id));
136
+ const locById = new Map(files.map((f) => [f.id, f.loc]));
137
+ // `resolution` (override-tunable, default 1) trades coarse↔fine modules —
138
+ // raise it to split a too-coarse clustering (the backthread dogfood uses >1).
139
+ const resolution = overrides?.resolution ?? 1;
140
+ // `assign` overrides (forced membership) win over ANY community detection,
141
+ // on both the whole-graph and per-package paths. Forced files still sit in
142
+ // the Louvain graph as nodes (their edges shape the partition around them,
143
+ // exactly as pre-Stage-C) — only their module ASSIGNMENT is overridden.
144
+ const forced = new Map(); // fileId → overridden moduleId
145
+ for (const f of files) {
146
+ const override = assign.find((a) => a.match(f.id));
147
+ if (override)
148
+ forced.set(f.id, override.moduleId);
149
+ }
150
+ const groups = [];
151
+ const layout = opts?.layout;
152
+ const multiPackage = layout !== undefined && layout.packages.length > 1;
153
+ if (!multiPackage) {
154
+ // ── Whole-graph path (pre-Stage-C, byte-identical) ─────────────────────
155
+ // Build the undirected weighted graph for Louvain (internal↔internal edges).
156
+ const g = new Graph({ type: 'undirected' });
157
+ for (const f of files)
158
+ g.addNode(f.id);
159
+ for (const e of graph.edges) {
160
+ if (e.external)
161
+ continue;
162
+ if (!fileSet.has(e.from) || !fileSet.has(e.to))
163
+ continue;
164
+ if (g.hasEdge(e.from, e.to)) {
165
+ g.updateEdgeAttribute(e.from, e.to, 'weight', (w) => (w ?? 1) + e.weight);
166
+ }
167
+ else {
168
+ g.addEdge(e.from, e.to, { weight: e.weight });
169
+ }
170
+ }
171
+ // Community detection → fileId → community index. Seeded RNG for stable ids.
172
+ const communities = g.order > 0
173
+ ? louvain(g, { getEdgeWeight: 'weight', resolution, rng: mulberry32(42) })
174
+ : {};
175
+ const byCommunity = new Map();
176
+ for (const f of files) {
177
+ if (forced.has(f.id))
178
+ continue;
179
+ const c = communities[f.id] ?? -1;
180
+ const arr = byCommunity.get(c) ?? [];
181
+ arr.push(f.id);
182
+ byCommunity.set(c, arr);
183
+ }
184
+ for (const fileIds of byCommunity.values())
185
+ groups.push({ pkg: null, fileIds, sole: false });
186
+ }
187
+ else {
188
+ // ── Per-package path ( Stage C) ─────────────────────────────────
189
+ // Louvain runs on the subgraph induced by edges with BOTH endpoints in the
190
+ // same package. Cross-package edges are EXCLUDED from community detection
191
+ // — the boundary is declared by the manifest, not discovered statistically
192
+ // — but they still aggregate to module edges below, at full
193
+ // confidence like any other edge.
194
+ const ownerRoot = new Map(); // fileId → owning package root
195
+ const membersByRoot = new Map();
196
+ for (const f of files) {
197
+ const r = layout.packageOf(f.id).root;
198
+ ownerRoot.set(f.id, r);
199
+ const arr = membersByRoot.get(r) ?? [];
200
+ arr.push(f.id);
201
+ membersByRoot.set(r, arr);
202
+ }
203
+ const intraEdgesByRoot = new Map();
204
+ for (const e of graph.edges) {
205
+ if (e.external)
206
+ continue;
207
+ if (!fileSet.has(e.from) || !fileSet.has(e.to))
208
+ continue;
209
+ const r = ownerRoot.get(e.from);
210
+ if (r !== ownerRoot.get(e.to))
211
+ continue; // cross-package: not detection input
212
+ const arr = intraEdgesByRoot.get(r) ?? [];
213
+ arr.push(e);
214
+ intraEdgesByRoot.set(r, arr);
215
+ }
216
+ // Packages in root-sorted order — module materialization order (and the
217
+ // id-dedup order downstream of it) must not depend on layout insertion
218
+ // order. Determinism is load-bearing everywhere in this file.
219
+ const sortedPkgs = [...layout.packages].sort((a, b) => a.root < b.root ? -1 : a.root > b.root ? 1 : 0);
220
+ for (const pkg of sortedPkgs) {
221
+ const members = membersByRoot.get(pkg.root) ?? [];
222
+ if (members.length === 0)
223
+ continue;
224
+ // Warm reuse: clean package + identical member set ⇒ the stored
225
+ // partition is the partition (see PackagePartitionCache for why).
226
+ const memberKey = [...members].sort().join('\n');
227
+ let communities = opts?.partitionCache?.lookup(pkg.slug, memberKey) ?? null;
228
+ if (communities === null) {
229
+ const sub = new Graph({ type: 'undirected' });
230
+ for (const id of members)
231
+ sub.addNode(id);
232
+ for (const e of intraEdgesByRoot.get(pkg.root) ?? []) {
233
+ if (sub.hasEdge(e.from, e.to)) {
234
+ sub.updateEdgeAttribute(e.from, e.to, 'weight', (w) => (w ?? 1) + e.weight);
235
+ }
236
+ else {
237
+ sub.addEdge(e.from, e.to, { weight: e.weight });
238
+ }
239
+ }
240
+ // Same seeded RNG per package: a package's partition depends only on
241
+ // its own subgraph, never on sibling packages — that's the stability
242
+ // win over whole-graph Louvain. (Edgeless subgraphs are handled by the
243
+ // library: every node becomes its own community, in insertion order.)
244
+ communities = louvain(sub, { getEdgeWeight: 'weight', resolution, rng: mulberry32(42) });
245
+ opts?.partitionCache?.store(pkg.slug, memberKey, communities);
246
+ }
247
+ const byCommunity = new Map();
248
+ for (const id of members) {
249
+ if (forced.has(id))
250
+ continue;
251
+ const c = communities[id] ?? -1;
252
+ const arr = byCommunity.get(c) ?? [];
253
+ arr.push(id);
254
+ byCommunity.set(c, arr);
255
+ }
256
+ const sole = byCommunity.size === 1;
257
+ for (const fileIds of byCommunity.values())
258
+ groups.push({ pkg, fileIds, sole });
259
+ }
260
+ }
261
+ // Materialise internal modules.
262
+ const fileModuleMap = {};
263
+ const modules = [];
264
+ const usedIds = new Set();
265
+ // Forced-assignment modules first (so their ids are reserved).
266
+ const forcedGroups = new Map();
267
+ for (const [fileId, moduleId] of forced) {
268
+ const arr = forcedGroups.get(moduleId) ?? [];
269
+ arr.push(fileId);
270
+ forcedGroups.set(moduleId, arr);
271
+ }
272
+ for (const [moduleId, fileIds] of forcedGroups) {
273
+ usedIds.add(moduleId);
274
+ for (const f of fileIds)
275
+ fileModuleMap[f] = moduleId;
276
+ modules.push(makeInternalModule(moduleId, fileIds, locById));
277
+ }
278
+ // Community modules. Ids: root-scope communities keep the bare derived id
279
+ // (pre-Stage-C behavior); a non-root package that is ONE community gets the
280
+ // package slug itself (the package IS the module); a multi-community package
281
+ // gets `<slug>-<derived>` with the derivation on PACKAGE-RELATIVE paths (so
282
+ // `worker/container/src/foo.ts` derives from `src/foo.ts`, not `worker`).
283
+ for (const grp of groups) {
284
+ const { pkg, fileIds } = grp;
285
+ let id;
286
+ if (pkg === null || pkg.root === '') {
287
+ id = deriveModuleId(fileIds, usedIds);
288
+ }
289
+ else if (grp.sole) {
290
+ id = reserveId(pkg.slug, usedIds);
291
+ }
292
+ else {
293
+ const rel = fileIds.map((f) => f.startsWith(`${pkg.root}/`) ? f.slice(pkg.root.length + 1) : f);
294
+ const seg = slugify(deriveSegment(rel)) || 'module';
295
+ id = reserveId(`${pkg.slug}-${seg}`, usedIds);
296
+ }
297
+ for (const f of fileIds)
298
+ fileModuleMap[f] = id;
299
+ const mod = makeInternalModule(id, fileIds, locById);
300
+ if (pkg !== null && pkg.root !== '') {
301
+ mod.packageId = pkg.slug;
302
+ mod.packageName = pkg.name; // humanized for the subsystem box label
303
+ mod.packageRole = pkg.role; // app / lib / tooling
304
+ }
305
+ modules.push(mod);
306
+ }
307
+ // Module-id stabilization ( Stage C warm-start) — AFTER modules are
308
+ // materialized, BEFORE edge aggregation + god-node detection (both read the
309
+ // FINAL ids) and before the workspace remap below (entry-module resolution
310
+ // must see stabilized ids).
311
+ if (opts?.priorModules !== undefined && opts.priorModules.length > 0) {
312
+ stabilizeModuleIds(modules, fileModuleMap, opts.priorModules, {
313
+ forcedIds: new Set(forcedGroups.keys()),
314
+ });
315
+ }
316
+ // Workspace-name external remap ( Stage C — the cross-package edge
317
+ // win). The install-free extractor sees `import x from '@org/pkg-b'` as a
318
+ // bare specifier → an EXTERNAL node, even when pkg-b is a workspace sibling
319
+ // sitting right there in the repo. With a layout we know better: map each
320
+ // named non-root package's external id to its ENTRY module (first entry
321
+ // candidate present in the file→module map). Unresolvable entry → no remap,
322
+ // the external stays (fail-soft — never guess a target).
323
+ const workspaceEntryByExtId = new Map();
324
+ if (layout !== undefined) {
325
+ for (const pkg of layout.nameToPackage.values()) {
326
+ const extId = externalIdFor(pkg.name).id;
327
+ const entry = pkg.entryFileIds.find((f) => fileModuleMap[f] !== undefined);
328
+ if (entry !== undefined)
329
+ workspaceEntryByExtId.set(extId, fileModuleMap[entry]);
330
+ }
331
+ }
332
+ // External modules — only those still referenced by a SURVIVING (non-dropped)
333
+ // file. An external introduced solely by a dropped file (e.g. eslint via
334
+ // eslint.config.js, vitest via a *.test.ts) must not leak onto the diagram.
335
+ // A workspace-remapped external must NOT materialize either — its edges now
336
+ // point at the sibling package's entry module, so an external box for it
337
+ // would be a phantom duplicate of an internal module.
338
+ const referencedExternals = new Set(graph.edges.filter((e) => e.external && fileSet.has(e.from)).map((e) => e.to));
339
+ for (const ext of graph.externals) {
340
+ if (!referencedExternals.has(ext.id))
341
+ continue;
342
+ if (workspaceEntryByExtId.has(ext.id))
343
+ continue;
344
+ modules.push({
345
+ id: ext.id,
346
+ kind: 'external',
347
+ fileIds: [],
348
+ externalSpecifier: ext.specifier,
349
+ fileCount: 0,
350
+ loc: 0,
351
+ degree: 0,
352
+ godNode: false,
353
+ });
354
+ }
355
+ // Aggregate file edges → module edges (preserve direction). Cross-package
356
+ // edges aggregate here like any other edge — full confidence, no penalty for
357
+ // having been excluded from community detection.
358
+ const moduleEdges = aggregateModuleEdges(graph, fileModuleMap, fileSet, workspaceEntryByExtId);
359
+ // declared cross-package dependency edges. The import graph only sees
360
+ // a package→package dependency when there's a resolved import; package.json
361
+ // sibling-workspace deps (collected as pkg.declaredDeps in workspaces.ts)
362
+ // declare the relation even when no import resolved. Materialize
363
+ // each as a structural `import` edge between the two packages' ENTRY modules
364
+ // (→ semantic `calls` downstream; assemble defaults the verb to `calls`). Only
365
+ // FILLS GAPS: a pair already joined by an aggregated import edge is left alone
366
+ // (no weight inflation). A self-edge is skipped silently; an UNRESOLVED entry
367
+ // module is skipped fail-soft but LOGGED (no silent caps — a missing edge must
368
+ // be distinguishable from "no dependency"). Deterministic: packages
369
+ // root-sorted, declaredDeps pre-sorted.
370
+ if (multiPackage && layout !== undefined) {
371
+ const pkgByRoot = new Map(layout.packages.map((p) => [p.root, p]));
372
+ const entryModuleOf = (pkg) => {
373
+ const entry = pkg.entryFileIds.find((f) => fileModuleMap[f] !== undefined);
374
+ return entry !== undefined ? fileModuleMap[entry] : undefined;
375
+ };
376
+ const existing = new Set(moduleEdges.map((e) => `${e.source} ${e.target}`));
377
+ const unresolved = []; // declared deps that couldn't anchor to an entry module
378
+ const sortedPkgs = [...layout.packages].sort((a, b) => a.root < b.root ? -1 : a.root > b.root ? 1 : 0);
379
+ for (const pkg of sortedPkgs) {
380
+ if (pkg.root === '' || pkg.declaredDeps.length === 0)
381
+ continue;
382
+ const src = entryModuleOf(pkg);
383
+ if (src === undefined) {
384
+ // pkg's own entry is unresolved → none of its declared deps can anchor.
385
+ for (const depRoot of pkg.declaredDeps)
386
+ unresolved.push(`${pkg.root}→${depRoot}`);
387
+ continue;
388
+ }
389
+ for (const depRoot of pkg.declaredDeps) {
390
+ const depPkg = pkgByRoot.get(depRoot);
391
+ if (depPkg === undefined)
392
+ continue; // structurally impossible (sibling root)
393
+ const tgt = entryModuleOf(depPkg);
394
+ if (tgt === undefined) {
395
+ unresolved.push(`${pkg.root}→${depRoot}`);
396
+ continue;
397
+ }
398
+ if (tgt === src)
399
+ continue; // legitimate self-edge, not an unresolved entry
400
+ const key = `${src} ${tgt}`;
401
+ if (existing.has(key))
402
+ continue;
403
+ existing.add(key);
404
+ moduleEdges.push({ source: src, target: tgt, weight: 1, kinds: ['import'] });
405
+ }
406
+ }
407
+ if (unresolved.length > 0) {
408
+ console.warn(` [workspaces] ${unresolved.length} declared cross-package dep(s) skipped — entry module unresolved: ${unresolved.join(', ')}`);
409
+ }
410
+ }
411
+ // Module-level degree + god-node flag. Degree counts distinct INTERNAL
412
+ // neighbours only — coupling to other internal modules is the scary-to-refactor
413
+ // signal. Counting external deps would flag any module that imports several
414
+ // libraries (a false positive that swamped the first dogfood run).
415
+ const internalIds = new Set(modules.filter((m) => m.kind === 'internal').map((m) => m.id));
416
+ const internalNodes = [...internalIds];
417
+ const neighbours = new Map();
418
+ for (const id of internalNodes)
419
+ setInto(neighbours, id);
420
+ for (const me of moduleEdges) {
421
+ if (!internalIds.has(me.source) || !internalIds.has(me.target))
422
+ continue;
423
+ neighbours.get(me.source).add(me.target);
424
+ neighbours.get(me.target).add(me.source);
425
+ }
426
+ for (const m of modules)
427
+ m.degree = neighbours.get(m.id)?.size ?? 0;
428
+ // Statistical god-node flag. Two conditions, ANDed:
429
+ // 1. internal-degree z-score > 1.5σ above the mean (an outlier in coupling)
430
+ // 2. betweenness centrality in the top quartile (a routing chokepoint)
431
+ // Only over the internal-module subgraph, and only when there are enough
432
+ // modules for the statistics to mean anything.
433
+ const godNodes = detectGodNodes(internalNodes, neighbours);
434
+ for (const m of modules)
435
+ m.godNode = m.kind === 'internal' && godNodes.has(m.id);
436
+ return { modules, fileModuleMap, moduleEdges };
437
+ }
438
+ // ---------------------------------------------------------------------------
439
+ // Module-id stabilization ( Stage C).
440
+ //
441
+ // WHY: module ids are the stable join key for the changelog AND the identity
442
+ // the enrich set-diff (enrich/diff.ts) reuses prior LLM labels by — a fresh
443
+ // cluster that produces the same community under a different derived id reads
444
+ // as "new module" downstream, which re-spends LLM tokens on a label we already
445
+ // have and churns the rendered topology for no real change. Stabilization
446
+ // greedily matches fresh internal modules to the prior extraction's internal
447
+ // modules by file-set overlap and adopts the prior ids.
448
+ //
449
+ // Rules (locked by the Stage C design):
450
+ // - candidate pairs (fresh internal × prior internal) scored by Jaccard
451
+ // overlap of fileId sets; sorted desc; accepted at ≥ 0.5, each fresh
452
+ // module and each prior id consumed at most once (greedy best-match);
453
+ // - override-FORCED modules are never renamed (their ids are user-authored)
454
+ // and their ids are never stolen by a match;
455
+ // - a module already bearing its matched prior id is not renamed (but the
456
+ // pair still consumes the id, so no other module can steal it);
457
+ // - on collision (a matched module adopts a prior id that ANOTHER fresh
458
+ // module derived naturally), the matched module wins; the other reroutes
459
+ // through the same `-2` dedup scheme deriveModuleId uses.
460
+ //
461
+ // Mutates `modules` (ids) and `fileModuleMap` in place, consistently —
462
+ // `packageId` and every other field are left intact. Callers must run this
463
+ // BEFORE aggregating module edges (clusterGraph does).
464
+ export function stabilizeModuleIds(modules, fileModuleMap, priorModules, opts) {
465
+ const forcedIds = opts?.forcedIds ?? new Set();
466
+ const priorInternal = priorModules.filter((m) => m.kind === 'internal');
467
+ if (priorInternal.length === 0)
468
+ return;
469
+ // Ids that can neither be renamed nor adopted: forced modules (user-
470
+ // authored) and any non-internal module a caller happens to pass. NOTE
471
+ // (REVIEWER PR #150): at clusterGraph's call site externals are appended to
472
+ // `modules` AFTER this runs, so they are NOT in this set there — external
473
+ // collisions are instead impossible by namespace (`ext:`-prefixed ids can
474
+ // never equal a slugified internal id; slugify strips ':'), and the guard
475
+ // below makes that invariant local rather than two files away.
476
+ const immovableIds = new Set();
477
+ for (const m of modules) {
478
+ if (m.kind !== 'internal' || forcedIds.has(m.id))
479
+ immovableIds.add(m.id);
480
+ }
481
+ const candidates = modules.filter((m) => m.kind === 'internal' && !forcedIds.has(m.id));
482
+ const pairs = [];
483
+ for (const mod of candidates) {
484
+ const fresh = new Set(mod.fileIds);
485
+ for (const prior of priorInternal) {
486
+ if (immovableIds.has(prior.id))
487
+ continue;
488
+ // Defense-in-depth (see immovableIds note): a namespaced id (`ext:…`)
489
+ // must never be adopted as an internal module id, regardless of what a
490
+ // caller put in priorModules or when externals join the modules array.
491
+ if (prior.id.includes(':'))
492
+ continue;
493
+ let inter = 0;
494
+ for (const f of prior.fileIds)
495
+ if (fresh.has(f))
496
+ inter++;
497
+ const union = fresh.size + prior.fileIds.length - inter;
498
+ const score = union === 0 ? 0 : inter / union;
499
+ if (score >= 0.5)
500
+ pairs.push({ mod, priorId: prior.id, score });
501
+ }
502
+ }
503
+ // Desc by score; ties broken by prior id then current id — fully
504
+ // deterministic regardless of input ordering (current ids are unique).
505
+ pairs.sort((a, b) => b.score - a.score ||
506
+ (a.priorId < b.priorId ? -1 : a.priorId > b.priorId ? 1 : 0) ||
507
+ (a.mod.id < b.mod.id ? -1 : a.mod.id > b.mod.id ? 1 : 0));
508
+ const usedPriorIds = new Set();
509
+ const matched = new Map(); // fresh module → adopted prior id
510
+ for (const p of pairs) {
511
+ if (usedPriorIds.has(p.priorId) || matched.has(p.mod))
512
+ continue;
513
+ usedPriorIds.add(p.priorId);
514
+ matched.set(p.mod, p.priorId);
515
+ }
516
+ if (matched.size === 0)
517
+ return;
518
+ // Compute final ids. Matched modules claim their prior ids first; immovable
519
+ // modules keep theirs; every remaining module keeps its current id unless a
520
+ // matched module claimed it — then it reroutes through the dedup suffix.
521
+ // Processing order is `modules` array order: deterministic.
522
+ const finalIds = new Map();
523
+ const used = new Set();
524
+ for (const m of modules) {
525
+ if (m.kind !== 'internal' || forcedIds.has(m.id)) {
526
+ finalIds.set(m, m.id);
527
+ used.add(m.id);
528
+ }
529
+ }
530
+ for (const [mod, priorId] of matched) {
531
+ finalIds.set(mod, priorId);
532
+ used.add(priorId);
533
+ }
534
+ for (const m of modules) {
535
+ if (finalIds.has(m))
536
+ continue;
537
+ finalIds.set(m, reserveId(m.id, used));
538
+ }
539
+ // Apply: rewrite module ids, then remap fileModuleMap by ORIGINAL value in
540
+ // one pass (original ids are unique, so a swap — A takes B's id while B
541
+ // reroutes — resolves correctly).
542
+ const idRemap = new Map();
543
+ for (const m of modules) {
544
+ const next = finalIds.get(m);
545
+ if (next !== m.id)
546
+ idRemap.set(m.id, next);
547
+ m.id = next;
548
+ }
549
+ if (idRemap.size === 0)
550
+ return;
551
+ for (const [fileId, moduleId] of Object.entries(fileModuleMap)) {
552
+ const next = idRemap.get(moduleId);
553
+ if (next !== undefined)
554
+ fileModuleMap[fileId] = next;
555
+ }
556
+ }
557
+ // Returns the set of internal module ids that are statistical god-nodes.
558
+ // Exported for unit testing (pure: adjacency in, flagged-set out).
559
+ export function detectGodNodes(nodes, adjacency) {
560
+ const flagged = new Set();
561
+ const n = nodes.length;
562
+ if (n < GOD_NODE_MIN_MODULES)
563
+ return flagged;
564
+ // (1) degree z-scores. Population std; if everyone has the same degree there
565
+ // is no outlier (std 0 → no flags).
566
+ const degrees = nodes.map((id) => adjacency.get(id)?.size ?? 0);
567
+ const mean = degrees.reduce((a, b) => a + b, 0) / n;
568
+ const variance = degrees.reduce((a, d) => a + (d - mean) ** 2, 0) / n;
569
+ const std = Math.sqrt(variance);
570
+ if (std === 0)
571
+ return flagged;
572
+ // (2) betweenness centrality (Brandes, unweighted undirected). The module
573
+ // graph is tiny (modules, not files), so the O(n·m) cost is negligible and an
574
+ // inline impl avoids pulling in graphology-metrics for ~25 lines.
575
+ const between = betweenness(nodes, adjacency);
576
+ const sorted = [...between.values()].sort((a, b) => a - b);
577
+ // Top-quartile cutoff = the 75th-percentile value. A node qualifies only if
578
+ // it's at/above the cutoff AND actually routes something (>0) — in a sparse
579
+ // graph many nodes have 0 betweenness and must not count as chokepoints.
580
+ const p75 = sorted[Math.floor(0.75 * (sorted.length - 1))];
581
+ for (const id of nodes) {
582
+ const z = ((adjacency.get(id)?.size ?? 0) - mean) / std;
583
+ const c = between.get(id) ?? 0;
584
+ if (z > GOD_NODE_Z_THRESHOLD && c > 0 && c >= p75)
585
+ flagged.add(id);
586
+ }
587
+ return flagged;
588
+ }
589
+ // Brandes' betweenness centrality for an unweighted, undirected graph.
590
+ // Deterministic (the centrality sums are independent of traversal order).
591
+ // Undirected → halve the final scores (each shortest path counted twice).
592
+ // Exported for unit testing.
593
+ export function betweenness(nodes, adjacency) {
594
+ const cb = new Map(nodes.map((id) => [id, 0]));
595
+ for (const s of nodes) {
596
+ const stack = [];
597
+ const pred = new Map(nodes.map((id) => [id, []]));
598
+ const sigma = new Map(nodes.map((id) => [id, 0]));
599
+ const dist = new Map(nodes.map((id) => [id, -1]));
600
+ sigma.set(s, 1);
601
+ dist.set(s, 0);
602
+ const queue = [s];
603
+ while (queue.length > 0) {
604
+ const v = queue.shift();
605
+ stack.push(v);
606
+ for (const w of adjacency.get(v) ?? []) {
607
+ if (dist.get(w) < 0) {
608
+ dist.set(w, dist.get(v) + 1);
609
+ queue.push(w);
610
+ }
611
+ if (dist.get(w) === dist.get(v) + 1) {
612
+ sigma.set(w, sigma.get(w) + sigma.get(v));
613
+ pred.get(w).push(v);
614
+ }
615
+ }
616
+ }
617
+ const delta = new Map(nodes.map((id) => [id, 0]));
618
+ while (stack.length > 0) {
619
+ const w = stack.pop();
620
+ for (const v of pred.get(w)) {
621
+ delta.set(v, delta.get(v) + (sigma.get(v) / sigma.get(w)) * (1 + delta.get(w)));
622
+ }
623
+ if (w !== s)
624
+ cb.set(w, cb.get(w) + delta.get(w));
625
+ }
626
+ }
627
+ for (const id of nodes)
628
+ cb.set(id, cb.get(id) / 2);
629
+ return cb;
630
+ }
631
+ function setInto(map, key) {
632
+ const s = new Set();
633
+ map.set(key, s);
634
+ return s;
635
+ }
636
+ function makeInternalModule(id, fileIds, locById) {
637
+ return {
638
+ id,
639
+ kind: 'internal',
640
+ fileIds,
641
+ fileCount: fileIds.length,
642
+ loc: fileIds.reduce((sum, f) => sum + (locById.get(f) ?? 0), 0),
643
+ degree: 0,
644
+ godNode: false,
645
+ };
646
+ }
647
+ function aggregateModuleEdges(graph, fileModuleMap, fileSet,
648
+ // Stage C: external id → workspace sibling's entry module id. A
649
+ // matching external edge becomes an INTERNAL module edge (kinds preserved,
650
+ // merged with any existing edge to the same pair). Self-edges (a package
651
+ // importing its own name) fall out via the src === tgt guard.
652
+ externalRemap) {
653
+ const edges = new Map();
654
+ for (const e of graph.edges) {
655
+ const src = fileModuleMap[e.from];
656
+ if (!src || !fileSet.has(e.from))
657
+ continue;
658
+ const tgt = e.external ? (externalRemap?.get(e.to) ?? e.to) : fileModuleMap[e.to];
659
+ if (!tgt || src === tgt)
660
+ continue;
661
+ const key = `${src} ${tgt}`;
662
+ const existing = edges.get(key);
663
+ if (existing) {
664
+ existing.weight += e.weight;
665
+ if (!existing.kinds.includes(e.kind))
666
+ existing.kinds.push(e.kind);
667
+ }
668
+ else {
669
+ edges.set(key, { source: src, target: tgt, weight: e.weight, kinds: [e.kind] });
670
+ }
671
+ }
672
+ return [...edges.values()];
673
+ }
674
+ //# sourceMappingURL=louvain.js.map