@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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/cluster/domain-grouping.d.ts +113 -0
- package/dist/cluster/domain-grouping.d.ts.map +1 -0
- package/dist/cluster/domain-grouping.js +277 -0
- package/dist/cluster/domain-grouping.js.map +1 -0
- package/dist/cluster/grouping-gate.d.ts +54 -0
- package/dist/cluster/grouping-gate.d.ts.map +1 -0
- package/dist/cluster/grouping-gate.js +0 -0
- package/dist/cluster/grouping-gate.js.map +1 -0
- package/dist/cluster/louvain.d.ts +88 -0
- package/dist/cluster/louvain.d.ts.map +1 -0
- package/dist/cluster/louvain.js +674 -0
- package/dist/cluster/louvain.js.map +1 -0
- package/dist/cluster/overrides.d.ts +19 -0
- package/dist/cluster/overrides.d.ts.map +1 -0
- package/dist/cluster/overrides.js +32 -0
- package/dist/cluster/overrides.js.map +1 -0
- package/dist/cluster/subsystem.d.ts +45 -0
- package/dist/cluster/subsystem.d.ts.map +1 -0
- package/dist/cluster/subsystem.js +299 -0
- package/dist/cluster/subsystem.js.map +1 -0
- package/dist/cluster/workspaces.d.ts +64 -0
- package/dist/cluster/workspaces.d.ts.map +1 -0
- package/dist/cluster/workspaces.js +739 -0
- package/dist/cluster/workspaces.js.map +1 -0
- package/dist/extract.d.ts +52 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +48 -0
- package/dist/extract.js.map +1 -0
- package/dist/framework/celery/celery.d.ts +17 -0
- package/dist/framework/celery/celery.d.ts.map +1 -0
- package/dist/framework/celery/celery.js +524 -0
- package/dist/framework/celery/celery.js.map +1 -0
- package/dist/framework/contribute-step.d.ts +38 -0
- package/dist/framework/contribute-step.d.ts.map +1 -0
- package/dist/framework/contribute-step.js +403 -0
- package/dist/framework/contribute-step.js.map +1 -0
- package/dist/framework/detect-step.d.ts +8 -0
- package/dist/framework/detect-step.d.ts.map +1 -0
- package/dist/framework/detect-step.js +54 -0
- package/dist/framework/detect-step.js.map +1 -0
- package/dist/framework/detect-util.d.ts +24 -0
- package/dist/framework/detect-util.d.ts.map +1 -0
- package/dist/framework/detect-util.js +59 -0
- package/dist/framework/detect-util.js.map +1 -0
- package/dist/framework/django/django.d.ts +22 -0
- package/dist/framework/django/django.d.ts.map +1 -0
- package/dist/framework/django/django.js +940 -0
- package/dist/framework/django/django.js.map +1 -0
- package/dist/framework/fastapi/fastapi.d.ts +27 -0
- package/dist/framework/fastapi/fastapi.d.ts.map +1 -0
- package/dist/framework/fastapi/fastapi.js +477 -0
- package/dist/framework/fastapi/fastapi.js.map +1 -0
- package/dist/framework/faststream/faststream.d.ts +19 -0
- package/dist/framework/faststream/faststream.d.ts.map +1 -0
- package/dist/framework/faststream/faststream.js +765 -0
- package/dist/framework/faststream/faststream.js.map +1 -0
- package/dist/framework/flask/flask.d.ts +17 -0
- package/dist/framework/flask/flask.d.ts.map +1 -0
- package/dist/framework/flask/flask.js +507 -0
- package/dist/framework/flask/flask.js.map +1 -0
- package/dist/framework/graphql/graphql.d.ts +17 -0
- package/dist/framework/graphql/graphql.d.ts.map +1 -0
- package/dist/framework/graphql/graphql.js +708 -0
- package/dist/framework/graphql/graphql.js.map +1 -0
- package/dist/framework/grpc/grpc.d.ts +22 -0
- package/dist/framework/grpc/grpc.d.ts.map +1 -0
- package/dist/framework/grpc/grpc.js +618 -0
- package/dist/framework/grpc/grpc.js.map +1 -0
- package/dist/framework/litestar/litestar.d.ts +17 -0
- package/dist/framework/litestar/litestar.d.ts.map +1 -0
- package/dist/framework/litestar/litestar.js +536 -0
- package/dist/framework/litestar/litestar.js.map +1 -0
- package/dist/framework/nest/nest.d.ts +19 -0
- package/dist/framework/nest/nest.d.ts.map +1 -0
- package/dist/framework/nest/nest.js +613 -0
- package/dist/framework/nest/nest.js.map +1 -0
- package/dist/framework/next/next.d.ts +22 -0
- package/dist/framework/next/next.d.ts.map +1 -0
- package/dist/framework/next/next.js +449 -0
- package/dist/framework/next/next.js.map +1 -0
- package/dist/framework/node/node.d.ts +24 -0
- package/dist/framework/node/node.d.ts.map +1 -0
- package/dist/framework/node/node.js +1009 -0
- package/dist/framework/node/node.js.map +1 -0
- package/dist/framework/orchestrator/orchestrator.d.ts +34 -0
- package/dist/framework/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/framework/orchestrator/orchestrator.js +691 -0
- package/dist/framework/orchestrator/orchestrator.js.map +1 -0
- package/dist/framework/orm/orm.d.ts +61 -0
- package/dist/framework/orm/orm.d.ts.map +1 -0
- package/dist/framework/orm/orm.js +620 -0
- package/dist/framework/orm/orm.js.map +1 -0
- package/dist/framework/python/analyze.d.ts +38 -0
- package/dist/framework/python/analyze.d.ts.map +1 -0
- package/dist/framework/python/analyze.js +98 -0
- package/dist/framework/python/analyze.js.map +1 -0
- package/dist/framework/python/py-ast.d.ts +130 -0
- package/dist/framework/python/py-ast.d.ts.map +1 -0
- package/dist/framework/python/py-ast.js +344 -0
- package/dist/framework/python/py-ast.js.map +1 -0
- package/dist/framework/python-orm/python-orm.d.ts +19 -0
- package/dist/framework/python-orm/python-orm.d.ts.map +1 -0
- package/dist/framework/python-orm/python-orm.js +516 -0
- package/dist/framework/python-orm/python-orm.js.map +1 -0
- package/dist/framework/react-native/react-native.d.ts +35 -0
- package/dist/framework/react-native/react-native.d.ts.map +1 -0
- package/dist/framework/react-native/react-native.js +1181 -0
- package/dist/framework/react-native/react-native.js.map +1 -0
- package/dist/framework/register.d.ts +2 -0
- package/dist/framework/register.d.ts.map +1 -0
- package/dist/framework/register.js +59 -0
- package/dist/framework/register.js.map +1 -0
- package/dist/framework/registry.d.ts +31 -0
- package/dist/framework/registry.d.ts.map +1 -0
- package/dist/framework/registry.js +70 -0
- package/dist/framework/registry.js.map +1 -0
- package/dist/framework/routing/expo-router.d.ts +31 -0
- package/dist/framework/routing/expo-router.d.ts.map +1 -0
- package/dist/framework/routing/expo-router.js +356 -0
- package/dist/framework/routing/expo-router.js.map +1 -0
- package/dist/framework/routing/index.d.ts +4 -0
- package/dist/framework/routing/index.d.ts.map +1 -0
- package/dist/framework/routing/index.js +16 -0
- package/dist/framework/routing/index.js.map +1 -0
- package/dist/framework/routing/next-router.d.ts +45 -0
- package/dist/framework/routing/next-router.d.ts.map +1 -0
- package/dist/framework/routing/next-router.js +378 -0
- package/dist/framework/routing/next-router.js.map +1 -0
- package/dist/framework/routing/route-tree.d.ts +73 -0
- package/dist/framework/routing/route-tree.d.ts.map +1 -0
- package/dist/framework/routing/route-tree.js +120 -0
- package/dist/framework/routing/route-tree.js.map +1 -0
- package/dist/framework/types.d.ts +230 -0
- package/dist/framework/types.d.ts.map +1 -0
- package/dist/framework/types.js +25 -0
- package/dist/framework/types.js.map +1 -0
- package/dist/graph/cross-language.d.ts +11 -0
- package/dist/graph/cross-language.d.ts.map +1 -0
- package/dist/graph/cross-language.js +123 -0
- package/dist/graph/cross-language.js.map +1 -0
- package/dist/graph/extract.d.ts +13 -0
- package/dist/graph/extract.d.ts.map +1 -0
- package/dist/graph/extract.js +79 -0
- package/dist/graph/extract.js.map +1 -0
- package/dist/graph/file-graph.d.ts +186 -0
- package/dist/graph/file-graph.d.ts.map +1 -0
- package/dist/graph/file-graph.js +427 -0
- package/dist/graph/file-graph.js.map +1 -0
- package/dist/graph/http-callsites.d.ts +14 -0
- package/dist/graph/http-callsites.d.ts.map +1 -0
- package/dist/graph/http-callsites.js +107 -0
- package/dist/graph/http-callsites.js.map +1 -0
- package/dist/graph/incremental.d.ts +122 -0
- package/dist/graph/incremental.d.ts.map +1 -0
- package/dist/graph/incremental.js +448 -0
- package/dist/graph/incremental.js.map +1 -0
- package/dist/graph/language.d.ts +31 -0
- package/dist/graph/language.d.ts.map +1 -0
- package/dist/graph/language.js +146 -0
- package/dist/graph/language.js.map +1 -0
- package/dist/graph/noise-filter.d.ts +46 -0
- package/dist/graph/noise-filter.d.ts.map +1 -0
- package/dist/graph/noise-filter.js +199 -0
- package/dist/graph/noise-filter.js.map +1 -0
- package/dist/graph/python-adapter.d.ts +36 -0
- package/dist/graph/python-adapter.d.ts.map +1 -0
- package/dist/graph/python-adapter.js +402 -0
- package/dist/graph/python-adapter.js.map +1 -0
- package/dist/graph/python-manifest.d.ts +22 -0
- package/dist/graph/python-manifest.d.ts.map +1 -0
- package/dist/graph/python-manifest.js +137 -0
- package/dist/graph/python-manifest.js.map +1 -0
- package/dist/graph/python-stdlib.d.ts +2 -0
- package/dist/graph/python-stdlib.d.ts.map +1 -0
- package/dist/graph/python-stdlib.js +53 -0
- package/dist/graph/python-stdlib.js.map +1 -0
- package/dist/graph/ts-morph-adapter.d.ts +65 -0
- package/dist/graph/ts-morph-adapter.d.ts.map +1 -0
- package/dist/graph/ts-morph-adapter.js +265 -0
- package/dist/graph/ts-morph-adapter.js.map +1 -0
- package/dist/graph/types.d.ts +37 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +50 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/aws/aws-parse.d.ts +39 -0
- package/dist/infra/aws/aws-parse.d.ts.map +1 -0
- package/dist/infra/aws/aws-parse.js +149 -0
- package/dist/infra/aws/aws-parse.js.map +1 -0
- package/dist/infra/aws/aws.d.ts +6 -0
- package/dist/infra/aws/aws.d.ts.map +1 -0
- package/dist/infra/aws/aws.js +425 -0
- package/dist/infra/aws/aws.js.map +1 -0
- package/dist/infra/azure/azure-parse.d.ts +73 -0
- package/dist/infra/azure/azure-parse.d.ts.map +1 -0
- package/dist/infra/azure/azure-parse.js +178 -0
- package/dist/infra/azure/azure-parse.js.map +1 -0
- package/dist/infra/azure/azure.d.ts +12 -0
- package/dist/infra/azure/azure.d.ts.map +1 -0
- package/dist/infra/azure/azure.js +673 -0
- package/dist/infra/azure/azure.js.map +1 -0
- package/dist/infra/cloudflare/cloudflare.d.ts +50 -0
- package/dist/infra/cloudflare/cloudflare.d.ts.map +1 -0
- package/dist/infra/cloudflare/cloudflare.js +382 -0
- package/dist/infra/cloudflare/cloudflare.js.map +1 -0
- package/dist/infra/cloudflare/wrangler-parse.d.ts +6 -0
- package/dist/infra/cloudflare/wrangler-parse.d.ts.map +1 -0
- package/dist/infra/cloudflare/wrangler-parse.js +278 -0
- package/dist/infra/cloudflare/wrangler-parse.js.map +1 -0
- package/dist/infra/compose/compose-parse.d.ts +24 -0
- package/dist/infra/compose/compose-parse.d.ts.map +1 -0
- package/dist/infra/compose/compose-parse.js +77 -0
- package/dist/infra/compose/compose-parse.js.map +1 -0
- package/dist/infra/compose/compose.d.ts +12 -0
- package/dist/infra/compose/compose.d.ts.map +1 -0
- package/dist/infra/compose/compose.js +201 -0
- package/dist/infra/compose/compose.js.map +1 -0
- package/dist/infra/convex/convex-parse.d.ts +28 -0
- package/dist/infra/convex/convex-parse.d.ts.map +1 -0
- package/dist/infra/convex/convex-parse.js +74 -0
- package/dist/infra/convex/convex-parse.js.map +1 -0
- package/dist/infra/convex/convex.d.ts +18 -0
- package/dist/infra/convex/convex.d.ts.map +1 -0
- package/dist/infra/convex/convex.js +199 -0
- package/dist/infra/convex/convex.js.map +1 -0
- package/dist/infra/digitalocean/digitalocean.d.ts +33 -0
- package/dist/infra/digitalocean/digitalocean.d.ts.map +1 -0
- package/dist/infra/digitalocean/digitalocean.js +204 -0
- package/dist/infra/digitalocean/digitalocean.js.map +1 -0
- package/dist/infra/firebase/firebase.d.ts +43 -0
- package/dist/infra/firebase/firebase.d.ts.map +1 -0
- package/dist/infra/firebase/firebase.js +271 -0
- package/dist/infra/firebase/firebase.js.map +1 -0
- package/dist/infra/fly/fly-parse.d.ts +52 -0
- package/dist/infra/fly/fly-parse.d.ts.map +1 -0
- package/dist/infra/fly/fly-parse.js +114 -0
- package/dist/infra/fly/fly-parse.js.map +1 -0
- package/dist/infra/fly/fly.d.ts +15 -0
- package/dist/infra/fly/fly.d.ts.map +1 -0
- package/dist/infra/fly/fly.js +265 -0
- package/dist/infra/fly/fly.js.map +1 -0
- package/dist/infra/gcp/gcp-parse.d.ts +24 -0
- package/dist/infra/gcp/gcp-parse.d.ts.map +1 -0
- package/dist/infra/gcp/gcp-parse.js +123 -0
- package/dist/infra/gcp/gcp-parse.js.map +1 -0
- package/dist/infra/gcp/gcp.d.ts +10 -0
- package/dist/infra/gcp/gcp.d.ts.map +1 -0
- package/dist/infra/gcp/gcp.js +502 -0
- package/dist/infra/gcp/gcp.js.map +1 -0
- package/dist/infra/heroku/heroku-parse.d.ts +33 -0
- package/dist/infra/heroku/heroku-parse.d.ts.map +1 -0
- package/dist/infra/heroku/heroku-parse.js +63 -0
- package/dist/infra/heroku/heroku-parse.js.map +1 -0
- package/dist/infra/heroku/heroku.d.ts +24 -0
- package/dist/infra/heroku/heroku.d.ts.map +1 -0
- package/dist/infra/heroku/heroku.js +253 -0
- package/dist/infra/heroku/heroku.js.map +1 -0
- package/dist/infra/image-resolve.d.ts +56 -0
- package/dist/infra/image-resolve.d.ts.map +1 -0
- package/dist/infra/image-resolve.js +229 -0
- package/dist/infra/image-resolve.js.map +1 -0
- package/dist/infra/infra-step.d.ts +39 -0
- package/dist/infra/infra-step.d.ts.map +1 -0
- package/dist/infra/infra-step.js +57 -0
- package/dist/infra/infra-step.js.map +1 -0
- package/dist/infra/k8s/index.d.ts +78 -0
- package/dist/infra/k8s/index.d.ts.map +1 -0
- package/dist/infra/k8s/index.js +251 -0
- package/dist/infra/k8s/index.js.map +1 -0
- package/dist/infra/kamal/kamal-parse.d.ts +23 -0
- package/dist/infra/kamal/kamal-parse.d.ts.map +1 -0
- package/dist/infra/kamal/kamal-parse.js +66 -0
- package/dist/infra/kamal/kamal-parse.js.map +1 -0
- package/dist/infra/kamal/kamal.d.ts +17 -0
- package/dist/infra/kamal/kamal.d.ts.map +1 -0
- package/dist/infra/kamal/kamal.js +162 -0
- package/dist/infra/kamal/kamal.js.map +1 -0
- package/dist/infra/netlify/netlify.d.ts +33 -0
- package/dist/infra/netlify/netlify.d.ts.map +1 -0
- package/dist/infra/netlify/netlify.js +247 -0
- package/dist/infra/netlify/netlify.js.map +1 -0
- package/dist/infra/opentofu/opentofu.d.ts +3 -0
- package/dist/infra/opentofu/opentofu.d.ts.map +1 -0
- package/dist/infra/opentofu/opentofu.js +157 -0
- package/dist/infra/opentofu/opentofu.js.map +1 -0
- package/dist/infra/pulumi/pulumi-parse.d.ts +60 -0
- package/dist/infra/pulumi/pulumi-parse.d.ts.map +1 -0
- package/dist/infra/pulumi/pulumi-parse.js +155 -0
- package/dist/infra/pulumi/pulumi-parse.js.map +1 -0
- package/dist/infra/pulumi/pulumi.d.ts +23 -0
- package/dist/infra/pulumi/pulumi.d.ts.map +1 -0
- package/dist/infra/pulumi/pulumi.js +260 -0
- package/dist/infra/pulumi/pulumi.js.map +1 -0
- package/dist/infra/railway/railway-parse.d.ts +54 -0
- package/dist/infra/railway/railway-parse.d.ts.map +1 -0
- package/dist/infra/railway/railway-parse.js +200 -0
- package/dist/infra/railway/railway-parse.js.map +1 -0
- package/dist/infra/railway/railway.d.ts +19 -0
- package/dist/infra/railway/railway.d.ts.map +1 -0
- package/dist/infra/railway/railway.js +397 -0
- package/dist/infra/railway/railway.js.map +1 -0
- package/dist/infra/register.d.ts +2 -0
- package/dist/infra/register.d.ts.map +1 -0
- package/dist/infra/register.js +81 -0
- package/dist/infra/register.js.map +1 -0
- package/dist/infra/registry.d.ts +64 -0
- package/dist/infra/registry.d.ts.map +1 -0
- package/dist/infra/registry.js +205 -0
- package/dist/infra/registry.js.map +1 -0
- package/dist/infra/relevance.d.ts +16 -0
- package/dist/infra/relevance.d.ts.map +1 -0
- package/dist/infra/relevance.js +132 -0
- package/dist/infra/relevance.js.map +1 -0
- package/dist/infra/render/render-parse.d.ts +76 -0
- package/dist/infra/render/render-parse.d.ts.map +1 -0
- package/dist/infra/render/render-parse.js +155 -0
- package/dist/infra/render/render-parse.js.map +1 -0
- package/dist/infra/render/render.d.ts +11 -0
- package/dist/infra/render/render.d.ts.map +1 -0
- package/dist/infra/render/render.js +303 -0
- package/dist/infra/render/render.js.map +1 -0
- package/dist/infra/sst/sst-parse.d.ts +25 -0
- package/dist/infra/sst/sst-parse.d.ts.map +1 -0
- package/dist/infra/sst/sst-parse.js +45 -0
- package/dist/infra/sst/sst-parse.js.map +1 -0
- package/dist/infra/sst/sst.d.ts +19 -0
- package/dist/infra/sst/sst.d.ts.map +1 -0
- package/dist/infra/sst/sst.js +236 -0
- package/dist/infra/sst/sst.js.map +1 -0
- package/dist/infra/supabase/supabase-parse.d.ts +34 -0
- package/dist/infra/supabase/supabase-parse.d.ts.map +1 -0
- package/dist/infra/supabase/supabase-parse.js +113 -0
- package/dist/infra/supabase/supabase-parse.js.map +1 -0
- package/dist/infra/supabase/supabase.d.ts +15 -0
- package/dist/infra/supabase/supabase.d.ts.map +1 -0
- package/dist/infra/supabase/supabase.js +198 -0
- package/dist/infra/supabase/supabase.js.map +1 -0
- package/dist/infra/terraform/hcl-parse.d.ts +34 -0
- package/dist/infra/terraform/hcl-parse.d.ts.map +1 -0
- package/dist/infra/terraform/hcl-parse.js +203 -0
- package/dist/infra/terraform/hcl-parse.js.map +1 -0
- package/dist/infra/terraform/terraform.d.ts +14 -0
- package/dist/infra/terraform/terraform.d.ts.map +1 -0
- package/dist/infra/terraform/terraform.js +264 -0
- package/dist/infra/terraform/terraform.js.map +1 -0
- package/dist/infra/types.d.ts +151 -0
- package/dist/infra/types.d.ts.map +1 -0
- package/dist/infra/types.js +14 -0
- package/dist/infra/types.js.map +1 -0
- package/dist/infra/vercel/vercel-parse.d.ts +70 -0
- package/dist/infra/vercel/vercel-parse.d.ts.map +1 -0
- package/dist/infra/vercel/vercel-parse.js +194 -0
- package/dist/infra/vercel/vercel-parse.js.map +1 -0
- package/dist/infra/vercel/vercel.d.ts +30 -0
- package/dist/infra/vercel/vercel.d.ts.map +1 -0
- package/dist/infra/vercel/vercel.js +523 -0
- package/dist/infra/vercel/vercel.js.map +1 -0
- package/dist/infra/walk.d.ts +36 -0
- package/dist/infra/walk.d.ts.map +1 -0
- package/dist/infra/walk.js +85 -0
- package/dist/infra/walk.js.map +1 -0
- package/dist/safety.d.ts +13 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +67 -0
- package/dist/safety.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +111 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +35 -0
- package/dist/version.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
// the FastStream (+ Taskiq) FrameworkAdapter. Net-new adapter
|
|
2
|
+
// following the FastAPI / Flask template, reusing the shared
|
|
3
|
+
// Python core (py-ast + parsePythonScope). FastStream is the cleanest
|
|
4
|
+
// declarative pub/sub in Python: a broker object + `@broker.subscriber("topic")` /
|
|
5
|
+
// `@broker.publisher("topic")` decorators wire event-driven services through
|
|
6
|
+
// message brokers (Kafka / RabbitMQ / NATS / Redis). Taskiq is its distributed
|
|
7
|
+
// task-queue sibling (`@broker.task` def + `some_task.kiq(...)` enqueue).
|
|
8
|
+
//
|
|
9
|
+
// We read this surface STATICALLY (install-free, never-store-source — a pure
|
|
10
|
+
// syntactic Pyright parse; never executes repo code) and persist only the derived
|
|
11
|
+
// groups/edges/roles:
|
|
12
|
+
//
|
|
13
|
+
// * detect() — the `faststream` and/or `taskiq` dependency (either alone
|
|
14
|
+
// is a valid match; both raises confidence). Shallow nested
|
|
15
|
+
// scan for a `backend/`|`server/` package (mirrors ).
|
|
16
|
+
// * groupingPrior — one FrameworkGroup per FastStream *Router (KafkaRouter,
|
|
17
|
+
// RabbitRouter, …): its defining file + the files that
|
|
18
|
+
// register subscribers/publishers to it, so a repo that
|
|
19
|
+
// organises handlers behind routers splits into per-router
|
|
20
|
+
// subsystems (the FastAPI router / Flask blueprint mechanism).
|
|
21
|
+
// Optional — a broker-only app (no routers) yields no groups
|
|
22
|
+
// and stays in directory grouping, byte-for-byte unchanged.
|
|
23
|
+
// * syntheticEdges — the ASYNC wiring the import graph structurally cannot see:
|
|
24
|
+
// (a) PUB/SUB — a module that publishes to topic T is linked
|
|
25
|
+
// to every module subscribing to T (kind 'publishes',
|
|
26
|
+
// publisher-file → subscriber-file, topic in metadata),
|
|
27
|
+
// (b) Taskiq `.kiq()` enqueue (kind 'publishes', caller →
|
|
28
|
+
// task file — the async decoupling the import graph would
|
|
29
|
+
// render as a plain call), and
|
|
30
|
+
// (c) `broker.include_router(router)` mounting (kind 'calls').
|
|
31
|
+
// * roleTags — FastStream app / broker / router objects → `gateway` (the
|
|
32
|
+
// system's MESSAGE entrypoint, the boundary where external
|
|
33
|
+
// events enter — the pub/sub analogue of FastAPI's app →
|
|
34
|
+
// gateway request entrypoint); a `@broker.subscriber` /
|
|
35
|
+
// `@broker.publisher` handler → `service` (an event-driven
|
|
36
|
+
// microservice — FastStream's own positioning); a Taskiq
|
|
37
|
+
// `@broker.task` → `job` (discrete queue-triggered own-code,
|
|
38
|
+
// matching the Celery-task precedent in ). METADATA
|
|
39
|
+
// onto the LOCKED MODULE_KINDS enum; never a new kind (only
|
|
40
|
+
// `role` renders; the module's `kind` is unchanged).
|
|
41
|
+
//
|
|
42
|
+
// Kind justification (the parent-task brief asked to pick locked kinds + justify):
|
|
43
|
+
// gateway/service/job are the three request/message-plane MODULE_KINDS. The broker
|
|
44
|
+
// is where messages CROSS the trust boundary → gateway (Linear's issue text floated
|
|
45
|
+
// `service` for the broker; the message-entrypoint reading is more faithful and
|
|
46
|
+
// matches FastAPI). A subscriber is long-lived event-consuming own-code → `service`
|
|
47
|
+
// (not a discrete `job`); a Taskiq task IS a discrete enqueued work unit → `job`.
|
|
48
|
+
// This is a deliberate, meaningful split of all three kinds; NO new kind is added.
|
|
49
|
+
//
|
|
50
|
+
// Pub/sub edge DIRECTION + verb: a single directed edge publisher-file →
|
|
51
|
+
// subscriber-file, kind 'publishes'. The topic is the MATCH KEY, not a node
|
|
52
|
+
// (FrameworkEdge endpoints must be code file ids), so it can't be an intermediate.
|
|
53
|
+
// 'publishes' points in the direction the message travels (producer → consumer),
|
|
54
|
+
// consistent with the Celery-enqueue precedent (enqueuer `publishes` to
|
|
55
|
+
// worker) and the infra queue precedent (Cloudflare: producer `publishes`, ARP-…).
|
|
56
|
+
// `subscribes` (the reverse-direction verb) is NOT used for a producer→consumer
|
|
57
|
+
// flow edge — it would invert the data-flow arrow; the subscriber side is captured
|
|
58
|
+
// by roleTags + the edge's `topic` metadata.
|
|
59
|
+
//
|
|
60
|
+
// Truly-dynamic topic names (a variable / f-string, not a static literal) and
|
|
61
|
+
// unresolvable kiq / include_router targets DEGRADE + LOG — no silent caps.
|
|
62
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
63
|
+
import { join } from 'node:path';
|
|
64
|
+
import { clampConfidence, resolveBase } from '../detect-util.js';
|
|
65
|
+
import { readPythonDeps } from '../../graph/python-manifest.js';
|
|
66
|
+
import { parsePythonScope } from '../python/analyze.js';
|
|
67
|
+
import { callCallee, keywordArg, memberChain, nameValue, positionalArgs, stringValue, PN, } from '../python/py-ast.js';
|
|
68
|
+
/** Gather the signal set for a single root dir (reads manifests only). */
|
|
69
|
+
export function gatherFastStreamSignals(baseDir) {
|
|
70
|
+
const deps = readPythonDeps(baseDir);
|
|
71
|
+
return {
|
|
72
|
+
hasFastStream: deps.has('faststream'),
|
|
73
|
+
// `taskiq` core OR any `taskiq-*` broker/result-backend (taskiq-aio-pika,
|
|
74
|
+
// taskiq-redis, taskiq-nats, …) — the task pass is worth running for either.
|
|
75
|
+
hasTaskiq: deps.has('taskiq') || [...deps].some((d) => d.startsWith('taskiq-')),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Non-source dirs the nested scan skips (cheap + can't hold a first-party manifest).
|
|
79
|
+
const NESTED_SKIP_DIRS = new Set([
|
|
80
|
+
'node_modules',
|
|
81
|
+
'dist',
|
|
82
|
+
'build',
|
|
83
|
+
'out',
|
|
84
|
+
'coverage',
|
|
85
|
+
'venv',
|
|
86
|
+
'.venv',
|
|
87
|
+
'__pycache__',
|
|
88
|
+
'site-packages',
|
|
89
|
+
]);
|
|
90
|
+
/** True if `dir` holds a Python manifest worth scanning for deps. */
|
|
91
|
+
function hasPythonManifest(dir) {
|
|
92
|
+
return (existsSync(join(dir, 'pyproject.toml')) ||
|
|
93
|
+
existsSync(join(dir, 'setup.py')) ||
|
|
94
|
+
existsSync(join(dir, 'setup.cfg')) ||
|
|
95
|
+
existsSync(join(dir, 'requirements.txt')));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Immediate subdirs (depth 1) that contain a Python manifest — the shallow search
|
|
99
|
+
* for a nested backend (`backend/` | `server/` | `worker/`). Sorted, so the
|
|
100
|
+
* first-match pick is deterministic; skips dot-dirs + non-source dirs to stay cheap.
|
|
101
|
+
*/
|
|
102
|
+
function shallowManifestSubdirs(base) {
|
|
103
|
+
let entries;
|
|
104
|
+
try {
|
|
105
|
+
entries = readdirSync(base, { withFileTypes: true });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const e of entries) {
|
|
112
|
+
if (!e.isDirectory() || e.name.startsWith('.') || NESTED_SKIP_DIRS.has(e.name))
|
|
113
|
+
continue;
|
|
114
|
+
if (hasPythonManifest(join(base, e.name)))
|
|
115
|
+
out.push(e.name);
|
|
116
|
+
}
|
|
117
|
+
return out.sort();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Decide FastStream/Taskiq from the signal set. EITHER `faststream` (pub/sub) or
|
|
121
|
+
* `taskiq` (task queue) is a valid match; both present raises confidence. Returns
|
|
122
|
+
* null → generic-Python fallthrough, byte-for-byte unchanged. The metadata GATES
|
|
123
|
+
* the two analysis passes (faststream → pub/sub; taskiq → tasks), mirroring how
|
|
124
|
+
* FastAPI gates its Celery pass.
|
|
125
|
+
*/
|
|
126
|
+
export function scoreFastStream(s, rootPath = '') {
|
|
127
|
+
if (!s.hasFastStream && !s.hasTaskiq)
|
|
128
|
+
return null;
|
|
129
|
+
let confidence = 0.8;
|
|
130
|
+
if (s.hasFastStream && s.hasTaskiq)
|
|
131
|
+
confidence += 0.1;
|
|
132
|
+
return {
|
|
133
|
+
adapter: 'faststream',
|
|
134
|
+
confidence: clampConfidence(confidence),
|
|
135
|
+
rootPath,
|
|
136
|
+
metadata: {
|
|
137
|
+
faststream: s.hasFastStream,
|
|
138
|
+
taskiq: s.hasTaskiq,
|
|
139
|
+
signals: { faststream: s.hasFastStream, taskiq: s.hasTaskiq },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Collapse priority when one FILE carries several roles, AND downstream when
|
|
144
|
+
// several files of different roles land in one module. The construction entrypoints
|
|
145
|
+
// (app/broker) outrank the concrete handlers, which outrank the bare aggregator
|
|
146
|
+
// router — mirroring FastAPI's app > route-handler > router: a single-file app
|
|
147
|
+
// (`FastStream(broker)` + a `@broker.subscriber` in the same module) reads as the
|
|
148
|
+
// app entrypoint; a router file that ALSO declares a handler reads as the handler
|
|
149
|
+
// (the informative leaf) rather than the aggregator; a pure aggregator router (its
|
|
150
|
+
// handlers all elsewhere) reads as `router`. A subscriber outranks a publisher —
|
|
151
|
+
// where a message is consumed is a more informative label than where one leaves.
|
|
152
|
+
const ROLE_PRIORITY = {
|
|
153
|
+
app: 9,
|
|
154
|
+
broker: 8,
|
|
155
|
+
subscriber: 7,
|
|
156
|
+
publisher: 6,
|
|
157
|
+
router: 5,
|
|
158
|
+
task: 4,
|
|
159
|
+
};
|
|
160
|
+
const ROLE_KIND = {
|
|
161
|
+
app: 'gateway',
|
|
162
|
+
broker: 'gateway',
|
|
163
|
+
router: 'gateway',
|
|
164
|
+
subscriber: 'service',
|
|
165
|
+
publisher: 'service',
|
|
166
|
+
task: 'job',
|
|
167
|
+
};
|
|
168
|
+
// The FastStream app entrypoint constructor.
|
|
169
|
+
const APP_CTOR = 'FastStream';
|
|
170
|
+
// FastStream router constructors (a KNOWN set — a suffix match on `*Router` would
|
|
171
|
+
// wrongly catch FastAPI's `APIRouter` in a FastAPI+FastStream polyglot repo). This
|
|
172
|
+
// set includes the broker-integration `faststream.<broker>.fastapi` routers, which
|
|
173
|
+
// share these class names.
|
|
174
|
+
const ROUTER_CTORS = new Set(['KafkaRouter', 'RabbitRouter', 'NatsRouter', 'RedisRouter']);
|
|
175
|
+
// Known destination keyword args across FastStream brokers (Kafka topic, NATS
|
|
176
|
+
// subject, Redis channel/list/stream, RabbitMQ queue) — used to read a topic name
|
|
177
|
+
// from `@broker.subscriber(channel="…")` / `broker.publish(m, subject="…")` forms.
|
|
178
|
+
const TOPIC_KWARGS = ['topic', 'subject', 'channel', 'queue', 'list', 'stream'];
|
|
179
|
+
/**
|
|
180
|
+
* A broker constructor: FastStream brokers (KafkaBroker / RabbitBroker /
|
|
181
|
+
* NatsBroker / RedisBroker) AND Taskiq brokers (AioPikaBroker / ListQueueBroker /
|
|
182
|
+
* RedisStreamBroker / InMemoryBroker / PubSubBroker …) all end in `Broker`. A
|
|
183
|
+
* suffix match is safe here: detect() already gated on the faststream/taskiq dep,
|
|
184
|
+
* and we only ACT on a broker var via framework-specific methods (`.subscriber` /
|
|
185
|
+
* `.publisher` / `.task` / `.publish` / `.include_router`), so a stray unrelated
|
|
186
|
+
* `*Broker` yields nothing. FastStream vs Taskiq usage is disambiguated by method
|
|
187
|
+
* name (subscriber/publisher vs task/kiq), not by broker class.
|
|
188
|
+
*/
|
|
189
|
+
function isBrokerCtor(name) {
|
|
190
|
+
return name.endsWith('Broker');
|
|
191
|
+
}
|
|
192
|
+
// Memoized on the FrameworkContext OBJECT (not repoDir) so groupingPrior +
|
|
193
|
+
// syntheticEdges + roleTags share ONE parse, while the merge walk's per-checkpoint
|
|
194
|
+
// ctx gets a fresh analysis — no cross-tree staleness. Mirrors fastapi / flask.
|
|
195
|
+
const ANALYSIS_CACHE = new WeakMap();
|
|
196
|
+
function slugify(s) {
|
|
197
|
+
return s
|
|
198
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
201
|
+
.replace(/^-+|-+$/g, '');
|
|
202
|
+
}
|
|
203
|
+
function humanize(s) {
|
|
204
|
+
const words = s
|
|
205
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
206
|
+
.replace(/[_\-.]+/g, ' ')
|
|
207
|
+
.trim()
|
|
208
|
+
.split(/\s+/)
|
|
209
|
+
.filter(Boolean);
|
|
210
|
+
if (words.length === 0)
|
|
211
|
+
return s;
|
|
212
|
+
return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
213
|
+
}
|
|
214
|
+
// The label source for a router definer file — its basename, but a package-level
|
|
215
|
+
// router lives in `<pkg>/__init__.py`, whose basename ('__init__') is useless, so
|
|
216
|
+
// fall back to the package dir name (`app/orders/__init__.py` → 'orders'). Generic
|
|
217
|
+
// definer basenames (`router`/`broker`) get the same treatment.
|
|
218
|
+
function moduleLabelSource(fileId) {
|
|
219
|
+
const parts = fileId.split('/');
|
|
220
|
+
const last = (parts[parts.length - 1] ?? fileId).replace(/\.[^.]+$/, '');
|
|
221
|
+
if ((last === '__init__' || last === 'router' || last === 'broker') && parts.length >= 2) {
|
|
222
|
+
return parts[parts.length - 2];
|
|
223
|
+
}
|
|
224
|
+
return last;
|
|
225
|
+
}
|
|
226
|
+
function dirSegment(fileId) {
|
|
227
|
+
const slash = fileId.indexOf('/');
|
|
228
|
+
return slash > 0 ? slugify(fileId.slice(0, slash)) : 'root';
|
|
229
|
+
}
|
|
230
|
+
// Keep the highest-priority candidate role per file (deterministic, lexical
|
|
231
|
+
// tiebreak) — mirrors the fastapi/flask addRole + the contribute-step's collapse.
|
|
232
|
+
function addRole(map, fileId, role) {
|
|
233
|
+
const cur = map.get(fileId);
|
|
234
|
+
if (cur === undefined ||
|
|
235
|
+
ROLE_PRIORITY[role] > ROLE_PRIORITY[cur] ||
|
|
236
|
+
(ROLE_PRIORITY[role] === ROLE_PRIORITY[cur] && role < cur)) {
|
|
237
|
+
map.set(fileId, role);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function addEdge(edges, from, to, kind, relation, extra) {
|
|
241
|
+
if (from === to)
|
|
242
|
+
return; // intra-file wiring collapses; the step drops self-edges too
|
|
243
|
+
const key = `${from}→${to}:${kind}`;
|
|
244
|
+
if (!edges.has(key)) {
|
|
245
|
+
edges.set(key, {
|
|
246
|
+
source: from,
|
|
247
|
+
target: to,
|
|
248
|
+
kind,
|
|
249
|
+
metadata: { framework: 'faststream', relation, ...extra },
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// The constructor name of an `x = Ctor(...)` assignment RHS (last chain segment, so
|
|
254
|
+
// both `KafkaBroker(...)` and `faststream.kafka.KafkaBroker(...)` read as
|
|
255
|
+
// 'KafkaBroker'), or undefined.
|
|
256
|
+
function assignedCtorName(rhs) {
|
|
257
|
+
if (rhs.nodeType !== PN.Call)
|
|
258
|
+
return undefined;
|
|
259
|
+
const callee = callCallee(rhs);
|
|
260
|
+
if (!callee)
|
|
261
|
+
return undefined;
|
|
262
|
+
return callee.path.length ? callee.path[callee.path.length - 1] : callee.root;
|
|
263
|
+
}
|
|
264
|
+
// A decorator's callee chain (`@broker.subscriber("t")` → root 'broker', path
|
|
265
|
+
// ['subscriber']; `@broker.task` → root 'broker', path ['task']). The decorator
|
|
266
|
+
// expr is either a call (with args) or a bare name/attribute.
|
|
267
|
+
function decoratorChain(deco) {
|
|
268
|
+
const expr = deco.d.expr;
|
|
269
|
+
if (expr.nodeType === PN.Call)
|
|
270
|
+
return callCallee(expr);
|
|
271
|
+
return memberChain(expr);
|
|
272
|
+
}
|
|
273
|
+
// A single topic-name arg → its static string, unwrapping a broker-specific
|
|
274
|
+
// destination object (`RabbitQueue("orders")` / `PubSub("ch")` → its first
|
|
275
|
+
// positional string). Undefined when the arg isn't a static literal.
|
|
276
|
+
function topicFromExpr(expr) {
|
|
277
|
+
if (!expr)
|
|
278
|
+
return undefined;
|
|
279
|
+
const direct = stringValue(expr);
|
|
280
|
+
if (direct !== undefined)
|
|
281
|
+
return direct;
|
|
282
|
+
if (expr.nodeType === PN.Call) {
|
|
283
|
+
return stringValue(positionalArgs(expr)[0]);
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Collect the static topic name(s) named by a call, scanning positional args in
|
|
289
|
+
* `[positionalStart, positionalEnd)` plus the known destination keyword args.
|
|
290
|
+
* A `subscriber`/`publisher` DECORATOR scans all positionals (Kafka allows several
|
|
291
|
+
* topics: `@broker.subscriber("t1","t2")`); imperative `publish(message, dest, …)`
|
|
292
|
+
* scans ONLY the single dest slot (index 1) — a later positional (e.g. a Kafka
|
|
293
|
+
* partition key passed positionally) is NOT a topic. `dynamic` = a topic arg was
|
|
294
|
+
* present but NONE resolved to a static literal (→ degrade + log).
|
|
295
|
+
*/
|
|
296
|
+
function collectTopics(call, positionalStart, positionalEnd = Infinity) {
|
|
297
|
+
const topics = new Set();
|
|
298
|
+
const positionals = positionalArgs(call);
|
|
299
|
+
let sawArg = false;
|
|
300
|
+
for (let i = positionalStart; i < Math.min(positionals.length, positionalEnd); i++) {
|
|
301
|
+
sawArg = true;
|
|
302
|
+
const t = topicFromExpr(positionals[i]);
|
|
303
|
+
if (t)
|
|
304
|
+
topics.add(t);
|
|
305
|
+
}
|
|
306
|
+
for (const kw of TOPIC_KWARGS) {
|
|
307
|
+
const arg = keywordArg(call, kw);
|
|
308
|
+
if (arg === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
sawArg = true;
|
|
311
|
+
const t = topicFromExpr(arg);
|
|
312
|
+
if (t)
|
|
313
|
+
topics.add(t);
|
|
314
|
+
}
|
|
315
|
+
return { topics: [...topics].sort(), dynamic: topics.size === 0 && sawArg };
|
|
316
|
+
}
|
|
317
|
+
// The topic set named by a `@broker.subscriber(...)`/`@broker.publisher(...)`
|
|
318
|
+
// decorator (a bare, uncalled decorator names no topic).
|
|
319
|
+
function decoratorTopics(deco) {
|
|
320
|
+
const expr = deco.d.expr;
|
|
321
|
+
if (expr.nodeType !== PN.Call)
|
|
322
|
+
return { topics: [], dynamic: false };
|
|
323
|
+
return collectTopics(expr, 0);
|
|
324
|
+
}
|
|
325
|
+
// Router group id source: the router var name (when not the generic `router`/
|
|
326
|
+
// `broker`), else the definer's package/basename.
|
|
327
|
+
function routerGroupName(varName, definerFileId) {
|
|
328
|
+
if (varName && varName !== 'router' && varName !== 'broker')
|
|
329
|
+
return varName;
|
|
330
|
+
return moduleLabelSource(definerFileId);
|
|
331
|
+
}
|
|
332
|
+
// Assign each router its final, collision-free group id ORDER-INDEPENDENTLY:
|
|
333
|
+
// process seeds by key (definerFileId#varName) so the SMALLEST wins the bare slug,
|
|
334
|
+
// and later collisions take a `-<dirSegment>` then `-<n>` suffix. The order is
|
|
335
|
+
// stable (not an iteration index), so the id set is identical run-to-run — the
|
|
336
|
+
// snapshot grouping-stability invariant. (Mirrors flask's assignBlueprintGroups.)
|
|
337
|
+
function assignRouterGroups(seeds) {
|
|
338
|
+
const taken = new Set();
|
|
339
|
+
const ordered = [...seeds].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
340
|
+
const groups = [];
|
|
341
|
+
for (const seed of ordered) {
|
|
342
|
+
let id = seed.baseSlug;
|
|
343
|
+
if (taken.has(id))
|
|
344
|
+
id = `${seed.baseSlug}-${dirSegment(seed.definerFileId)}`;
|
|
345
|
+
let n = 2;
|
|
346
|
+
while (taken.has(id))
|
|
347
|
+
id = `${seed.baseSlug}-${n++}`;
|
|
348
|
+
taken.add(id);
|
|
349
|
+
groups.push({ id, label: seed.label, fileIds: [...seed.fileIds].sort() });
|
|
350
|
+
}
|
|
351
|
+
return groups.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
352
|
+
}
|
|
353
|
+
function analyzeFastStream(ctx) {
|
|
354
|
+
const { parsed } = parsePythonScope(ctx);
|
|
355
|
+
const faststream = ctx.match.metadata?.faststream === true;
|
|
356
|
+
const taskiq = ctx.match.metadata?.taskiq === true;
|
|
357
|
+
// Pass 1 — per-file app / broker / router object variables (file-scoped, which
|
|
358
|
+
// matches how these module-level singletons are used).
|
|
359
|
+
const appVarsByFile = new Map();
|
|
360
|
+
const brokerVarsByFile = new Map();
|
|
361
|
+
const routerVarsByFile = new Map();
|
|
362
|
+
for (const [id, file] of parsed) {
|
|
363
|
+
const appVars = new Set();
|
|
364
|
+
const brokerVars = new Set();
|
|
365
|
+
const routerVars = new Set();
|
|
366
|
+
for (const a of file.nodes.assignments) {
|
|
367
|
+
const target = nameValue(a.d.leftExpr);
|
|
368
|
+
if (!target)
|
|
369
|
+
continue;
|
|
370
|
+
const ctor = assignedCtorName(a.d.rightExpr);
|
|
371
|
+
if (!ctor)
|
|
372
|
+
continue;
|
|
373
|
+
if (ctor === APP_CTOR)
|
|
374
|
+
appVars.add(target);
|
|
375
|
+
else if (ROUTER_CTORS.has(ctor))
|
|
376
|
+
routerVars.add(target);
|
|
377
|
+
else if (isBrokerCtor(ctor))
|
|
378
|
+
brokerVars.add(target);
|
|
379
|
+
}
|
|
380
|
+
appVarsByFile.set(id, appVars);
|
|
381
|
+
brokerVarsByFile.set(id, brokerVars);
|
|
382
|
+
routerVarsByFile.set(id, routerVars);
|
|
383
|
+
}
|
|
384
|
+
// Definer-file sets, so an IMPORTED broker/app (`from app.broker import broker` +
|
|
385
|
+
// `@broker.subscriber` elsewhere — the common layout) is recognized cross-file.
|
|
386
|
+
const appDefinerFiles = new Set();
|
|
387
|
+
const brokerDefinerFiles = new Set();
|
|
388
|
+
for (const [f, v] of appVarsByFile)
|
|
389
|
+
if (v.size > 0)
|
|
390
|
+
appDefinerFiles.add(f);
|
|
391
|
+
for (const [f, v] of brokerVarsByFile)
|
|
392
|
+
if (v.size > 0)
|
|
393
|
+
brokerDefinerFiles.add(f);
|
|
394
|
+
// Router seeds (grouping) + definer index (cross-file decorator attribution).
|
|
395
|
+
// Routers are a FastStream construct → only seeded on the faststream signal, so a
|
|
396
|
+
// Taskiq-only repo yields no groups (and never mis-attributes a `.task` to one).
|
|
397
|
+
const seedsByKey = new Map();
|
|
398
|
+
const routersByDefiner = new Map();
|
|
399
|
+
if (faststream) {
|
|
400
|
+
for (const [id, routerVars] of routerVarsByFile) {
|
|
401
|
+
for (const varName of routerVars) {
|
|
402
|
+
const key = `${id}#${varName}`;
|
|
403
|
+
const name = routerGroupName(varName, id);
|
|
404
|
+
seedsByKey.set(key, {
|
|
405
|
+
key,
|
|
406
|
+
definerFileId: id,
|
|
407
|
+
varName,
|
|
408
|
+
baseSlug: slugify(name) || 'router',
|
|
409
|
+
label: humanize(name) || name,
|
|
410
|
+
fileIds: new Set([id]),
|
|
411
|
+
});
|
|
412
|
+
const list = routersByDefiner.get(id) ?? [];
|
|
413
|
+
list.push({ varName, key });
|
|
414
|
+
routersByDefiner.set(id, list);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const roleByFile = new Map();
|
|
419
|
+
const edges = new Map();
|
|
420
|
+
const pubTopicsByFile = new Map(); // file → topics it publishes
|
|
421
|
+
const subTopicsByFile = new Map(); // file → topics it subscribes
|
|
422
|
+
const diag = {
|
|
423
|
+
dynamicTopics: new Set(),
|
|
424
|
+
unresolvedKiq: new Set(),
|
|
425
|
+
unresolvedMounts: new Set(),
|
|
426
|
+
ambiguousRouters: new Set(),
|
|
427
|
+
};
|
|
428
|
+
function recordTopics(map, file, topics) {
|
|
429
|
+
if (topics.length === 0)
|
|
430
|
+
return;
|
|
431
|
+
let set = map.get(file);
|
|
432
|
+
if (!set) {
|
|
433
|
+
set = new Set();
|
|
434
|
+
map.set(file, set);
|
|
435
|
+
}
|
|
436
|
+
for (const t of topics)
|
|
437
|
+
set.add(t);
|
|
438
|
+
}
|
|
439
|
+
// A FastStream broker is a `*Broker` that is actually USED by FastStream (a
|
|
440
|
+
// `.subscriber`/`.publisher`/`.publish`/`.include_router` resolves to it) — as
|
|
441
|
+
// opposed to a Taskiq `*Broker` (used only by `.task`/`.kiq`), which shares the
|
|
442
|
+
// class-name suffix. Populated during the walk; consumed by the object-role pass
|
|
443
|
+
// so a Taskiq broker in a mixed faststream+taskiq repo is NOT tagged `gateway`.
|
|
444
|
+
const faststreamBrokerFiles = new Set();
|
|
445
|
+
// Classify a `@<root>.…` / `<root>.…()` object root: which router (by key), the
|
|
446
|
+
// broker (with its definer file), or the app — resolving same-file vars first,
|
|
447
|
+
// then import bindings to a definer/app file. Undefined ⇒ not a recognized object.
|
|
448
|
+
function resolveBrokerLike(thisFile, root, binds) {
|
|
449
|
+
if (routerVarsByFile.get(thisFile)?.has(root))
|
|
450
|
+
return { kind: 'router', key: `${thisFile}#${root}` };
|
|
451
|
+
if (brokerVarsByFile.get(thisFile)?.has(root))
|
|
452
|
+
return { kind: 'broker', definer: thisFile };
|
|
453
|
+
if (appVarsByFile.get(thisFile)?.has(root))
|
|
454
|
+
return { kind: 'app' };
|
|
455
|
+
const target = binds.get(root);
|
|
456
|
+
if (target) {
|
|
457
|
+
const routers = routersByDefiner.get(target);
|
|
458
|
+
if (routers && routers.length > 0) {
|
|
459
|
+
// Prefer the router whose var name matches the imported local name; else, if
|
|
460
|
+
// the definer holds exactly one router, it's unambiguous; else DEGRADE.
|
|
461
|
+
const exact = routers.find((r) => r.varName === root);
|
|
462
|
+
if (exact)
|
|
463
|
+
return { kind: 'router', key: exact.key };
|
|
464
|
+
if (routers.length === 1)
|
|
465
|
+
return { kind: 'router', key: routers[0].key };
|
|
466
|
+
diag.ambiguousRouters.add(`${thisFile}: @${root}.… (multiple routers in ${target})`);
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
if (brokerDefinerFiles.has(target))
|
|
470
|
+
return { kind: 'broker', definer: target };
|
|
471
|
+
if (appDefinerFiles.has(target))
|
|
472
|
+
return { kind: 'app' };
|
|
473
|
+
}
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
// Mark the broker a faststream usage resolved to as FastStream-used (drives the
|
|
477
|
+
// gateway role). A no-op for router/app results.
|
|
478
|
+
function markFastStreamBroker(resolved) {
|
|
479
|
+
if (resolved?.kind === 'broker')
|
|
480
|
+
faststreamBrokerFiles.add(resolved.definer);
|
|
481
|
+
}
|
|
482
|
+
for (const [id, file] of parsed) {
|
|
483
|
+
const binds = file.bindings;
|
|
484
|
+
// --- Decorators: subscriber / publisher (faststream) + task (taskiq).
|
|
485
|
+
for (const fn of file.nodes.functions) {
|
|
486
|
+
for (const deco of fn.d.decorators) {
|
|
487
|
+
const chain = decoratorChain(deco);
|
|
488
|
+
if (!chain || chain.path.length !== 1)
|
|
489
|
+
continue;
|
|
490
|
+
const method = chain.path[0];
|
|
491
|
+
if (faststream && (method === 'subscriber' || method === 'publisher')) {
|
|
492
|
+
const resolved = resolveBrokerLike(id, chain.root, binds);
|
|
493
|
+
if (!resolved)
|
|
494
|
+
continue; // not a recognized broker/router object
|
|
495
|
+
markFastStreamBroker(resolved);
|
|
496
|
+
const topics = decoratorTopics(deco);
|
|
497
|
+
if (topics.dynamic)
|
|
498
|
+
diag.dynamicTopics.add(`${id}: @${chain.root}.${method}(<dynamic>)`);
|
|
499
|
+
if (method === 'subscriber') {
|
|
500
|
+
addRole(roleByFile, id, 'subscriber');
|
|
501
|
+
recordTopics(subTopicsByFile, id, topics.topics);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
addRole(roleByFile, id, 'publisher');
|
|
505
|
+
recordTopics(pubTopicsByFile, id, topics.topics);
|
|
506
|
+
}
|
|
507
|
+
// A handler decorated on a ROUTER joins that router's subsystem.
|
|
508
|
+
if (resolved.kind === 'router')
|
|
509
|
+
seedsByKey.get(resolved.key)?.fileIds.add(id);
|
|
510
|
+
}
|
|
511
|
+
else if (taskiq && method === 'task') {
|
|
512
|
+
// `@broker.task` (bare or called) — unambiguously Taskiq (FastStream
|
|
513
|
+
// brokers have no `.task`). Gate on a recognized broker object.
|
|
514
|
+
if (resolveBrokerLike(id, chain.root, binds))
|
|
515
|
+
addRole(roleByFile, id, 'task');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// --- Assigned publisher objects: `pub = broker.publisher("t")` (a publisher
|
|
520
|
+
// used imperatively via `pub.publish(...)`; the topic is fixed at construction).
|
|
521
|
+
if (faststream) {
|
|
522
|
+
for (const a of file.nodes.assignments) {
|
|
523
|
+
const rhs = a.d.rightExpr;
|
|
524
|
+
if (rhs.nodeType !== PN.Call)
|
|
525
|
+
continue;
|
|
526
|
+
const callee = callCallee(rhs);
|
|
527
|
+
if (!callee || callee.path.length !== 1 || callee.path[0] !== 'publisher')
|
|
528
|
+
continue;
|
|
529
|
+
const resolved = resolveBrokerLike(id, callee.root, binds);
|
|
530
|
+
if (!resolved)
|
|
531
|
+
continue;
|
|
532
|
+
markFastStreamBroker(resolved);
|
|
533
|
+
const topics = collectTopics(rhs, 0);
|
|
534
|
+
if (topics.dynamic)
|
|
535
|
+
diag.dynamicTopics.add(`${id}: ${callee.root}.publisher(<dynamic>)`);
|
|
536
|
+
addRole(roleByFile, id, 'publisher');
|
|
537
|
+
recordTopics(pubTopicsByFile, id, topics.topics);
|
|
538
|
+
if (resolved.kind === 'router')
|
|
539
|
+
seedsByKey.get(resolved.key)?.fileIds.add(id);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// --- Calls: imperative publish + include_router (faststream) · kiq (taskiq).
|
|
543
|
+
for (const call of file.nodes.calls) {
|
|
544
|
+
const callee = callCallee(call);
|
|
545
|
+
if (!callee || callee.path.length !== 1)
|
|
546
|
+
continue; // want `obj.method(...)`
|
|
547
|
+
const method = callee.path[0];
|
|
548
|
+
const obj = callee.root;
|
|
549
|
+
if (faststream && method === 'publish') {
|
|
550
|
+
// `broker.publish(message, "topic")` — imperative publish.
|
|
551
|
+
const resolved = resolveBrokerLike(id, obj, binds);
|
|
552
|
+
if (!resolved)
|
|
553
|
+
continue;
|
|
554
|
+
markFastStreamBroker(resolved);
|
|
555
|
+
addRole(roleByFile, id, 'publisher');
|
|
556
|
+
const topics = collectTopics(call, 1, 2); // only the single dest slot
|
|
557
|
+
if (topics.dynamic)
|
|
558
|
+
diag.dynamicTopics.add(`${id}: ${obj}.publish(<dynamic>)`);
|
|
559
|
+
recordTopics(pubTopicsByFile, id, topics.topics);
|
|
560
|
+
}
|
|
561
|
+
else if (faststream && method === 'include_router') {
|
|
562
|
+
// `broker.include_router(router)` — mounting (a plain call in the diagram).
|
|
563
|
+
const resolvedObj = resolveBrokerLike(id, obj, binds);
|
|
564
|
+
if (!resolvedObj)
|
|
565
|
+
continue;
|
|
566
|
+
markFastStreamBroker(resolvedObj);
|
|
567
|
+
const arg = positionalArgs(call)[0];
|
|
568
|
+
const argRoot = arg ? memberChain(arg)?.root : undefined;
|
|
569
|
+
const target = argRoot
|
|
570
|
+
? routerVarsByFile.get(id)?.has(argRoot)
|
|
571
|
+
? id // same-file router → self-edge (dropped by addEdge)
|
|
572
|
+
: binds.get(argRoot)
|
|
573
|
+
: undefined;
|
|
574
|
+
if (target)
|
|
575
|
+
addEdge(edges, id, target, 'calls', 'include-router');
|
|
576
|
+
else
|
|
577
|
+
diag.unresolvedMounts.add(`${id}: ${obj}.include_router(…)`);
|
|
578
|
+
}
|
|
579
|
+
else if (taskiq && method === 'kiq') {
|
|
580
|
+
// `some_task.kiq(...)` — the callee root is the imported task function; the
|
|
581
|
+
// enqueue is the async decoupling the import graph would render as a call.
|
|
582
|
+
const target = binds.get(obj);
|
|
583
|
+
if (target)
|
|
584
|
+
addEdge(edges, id, target, 'publishes', 'taskiq-kiq');
|
|
585
|
+
else
|
|
586
|
+
diag.unresolvedKiq.add(`${id}: ${obj}.kiq(…)`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// --- Object files are gateways (the message entrypoint), even when their
|
|
591
|
+
// handlers live elsewhere. Deferred to a post-walk pass because the `broker` role
|
|
592
|
+
// is gated on the broker being FastStream-used (`faststreamBrokerFiles`, filled by
|
|
593
|
+
// the walk above) — a Taskiq broker in a mixed repo is a task dispatcher, not a
|
|
594
|
+
// message gateway, so it stays unroled. app (FastStream) + router (KafkaRouter …)
|
|
595
|
+
// are unambiguously FastStream, so they need only the faststream gate. addRole is
|
|
596
|
+
// priority-ordered, so combining these with the handler roles is order-independent.
|
|
597
|
+
if (faststream) {
|
|
598
|
+
for (const id of parsed.keys()) {
|
|
599
|
+
if (appVarsByFile.get(id).size > 0)
|
|
600
|
+
addRole(roleByFile, id, 'app');
|
|
601
|
+
if (routerVarsByFile.get(id).size > 0)
|
|
602
|
+
addRole(roleByFile, id, 'router');
|
|
603
|
+
if (brokerVarsByFile.get(id).size > 0 && faststreamBrokerFiles.has(id)) {
|
|
604
|
+
addRole(roleByFile, id, 'broker');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// --- Pub/sub topic-matching edges: publisher-file → subscriber-file (kind
|
|
609
|
+
// 'publishes'), one edge per (publisher, subscriber) pair that SHARES a topic;
|
|
610
|
+
// the shared topic name(s) ride in metadata. Iterate topic → publishers ×
|
|
611
|
+
// subscribers, all sorted, so the aggregation is order-independent.
|
|
612
|
+
const topicIndex = new Map();
|
|
613
|
+
for (const [file, topics] of pubTopicsByFile) {
|
|
614
|
+
for (const t of topics) {
|
|
615
|
+
let e = topicIndex.get(t);
|
|
616
|
+
if (!e) {
|
|
617
|
+
e = { pubs: new Set(), subs: new Set() };
|
|
618
|
+
topicIndex.set(t, e);
|
|
619
|
+
}
|
|
620
|
+
e.pubs.add(file);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
for (const [file, topics] of subTopicsByFile) {
|
|
624
|
+
for (const t of topics) {
|
|
625
|
+
let e = topicIndex.get(t);
|
|
626
|
+
if (!e) {
|
|
627
|
+
e = { pubs: new Set(), subs: new Set() };
|
|
628
|
+
topicIndex.set(t, e);
|
|
629
|
+
}
|
|
630
|
+
e.subs.add(file);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Nested map (publisher -> subscriber -> shared topics) — no string round-trip,
|
|
634
|
+
// so a file id can never be corrupted by a separator, and no non-printable byte
|
|
635
|
+
// lands in the source. Insertion order is stable (sorted topics/pubs/subs), so
|
|
636
|
+
// the resulting edge set is deterministic.
|
|
637
|
+
const pairTopics = new Map();
|
|
638
|
+
for (const topic of [...topicIndex.keys()].sort()) {
|
|
639
|
+
const { pubs, subs } = topicIndex.get(topic);
|
|
640
|
+
for (const p of [...pubs].sort()) {
|
|
641
|
+
for (const s of [...subs].sort()) {
|
|
642
|
+
if (p === s)
|
|
643
|
+
continue; // a passthrough file (pub+sub same topic) doesn't self-link
|
|
644
|
+
let bySub = pairTopics.get(p);
|
|
645
|
+
if (!bySub) {
|
|
646
|
+
bySub = new Map();
|
|
647
|
+
pairTopics.set(p, bySub);
|
|
648
|
+
}
|
|
649
|
+
let set = bySub.get(s);
|
|
650
|
+
if (!set) {
|
|
651
|
+
set = new Set();
|
|
652
|
+
bySub.set(s, set);
|
|
653
|
+
}
|
|
654
|
+
set.add(topic);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for (const [p, bySub] of pairTopics) {
|
|
659
|
+
for (const [s, topics] of bySub) {
|
|
660
|
+
addEdge(edges, p, s, 'publishes', 'pubsub', { topics: [...topics].sort() });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const groups = assignRouterGroups([...seedsByKey.values()]);
|
|
664
|
+
const roles = new Map();
|
|
665
|
+
for (const [fileId, role] of roleByFile) {
|
|
666
|
+
roles.set(fileId, {
|
|
667
|
+
role,
|
|
668
|
+
kind: ROLE_KIND[role],
|
|
669
|
+
priority: ROLE_PRIORITY[role],
|
|
670
|
+
metadata: { framework: 'faststream' },
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const sortedEdges = [...edges.values()].sort((a, b) => a.source < b.source
|
|
674
|
+
? -1
|
|
675
|
+
: a.source > b.source
|
|
676
|
+
? 1
|
|
677
|
+
: a.target < b.target
|
|
678
|
+
? -1
|
|
679
|
+
: a.target > b.target
|
|
680
|
+
? 1
|
|
681
|
+
: a.kind < b.kind
|
|
682
|
+
? -1
|
|
683
|
+
: a.kind > b.kind
|
|
684
|
+
? 1
|
|
685
|
+
: 0);
|
|
686
|
+
// Positive signal for validation (mirrors fastapi/flask's log line).
|
|
687
|
+
if (groups.length > 0 || roleByFile.size > 0 || sortedEdges.length > 0) {
|
|
688
|
+
console.log(` [faststream] ${roleByFile.size} role(s) · ${groups.length} router group(s) · ${sortedEdges.length} edge(s)`);
|
|
689
|
+
}
|
|
690
|
+
// No silent caps (locked): log everything that degraded.
|
|
691
|
+
const degraded = [];
|
|
692
|
+
if (diag.dynamicTopics.size > 0) {
|
|
693
|
+
degraded.push(`${diag.dynamicTopics.size} dynamic topic(s): ${[...diag.dynamicTopics].sort().slice(0, 10).join(' · ')}`);
|
|
694
|
+
}
|
|
695
|
+
if (diag.unresolvedKiq.size > 0) {
|
|
696
|
+
degraded.push(`${diag.unresolvedKiq.size} unresolvable kiq(s): ${[...diag.unresolvedKiq].sort().slice(0, 10).join(' · ')}`);
|
|
697
|
+
}
|
|
698
|
+
if (diag.unresolvedMounts.size > 0) {
|
|
699
|
+
degraded.push(`${diag.unresolvedMounts.size} unresolvable mount(s): ${[...diag.unresolvedMounts].sort().slice(0, 10).join(' · ')}`);
|
|
700
|
+
}
|
|
701
|
+
if (diag.ambiguousRouters.size > 0) {
|
|
702
|
+
degraded.push(`${diag.ambiguousRouters.size} ambiguous router attribution(s): ${[...diag.ambiguousRouters].sort().slice(0, 10).join(' · ')}`);
|
|
703
|
+
}
|
|
704
|
+
if (degraded.length > 0) {
|
|
705
|
+
console.log(` [faststream] degraded: ${degraded.join(' · ')} (logged, not silently dropped)`);
|
|
706
|
+
}
|
|
707
|
+
return { groups, edges: sortedEdges, roles };
|
|
708
|
+
}
|
|
709
|
+
function getAnalysis(ctx) {
|
|
710
|
+
let a = ANALYSIS_CACHE.get(ctx);
|
|
711
|
+
if (!a) {
|
|
712
|
+
a = analyzeFastStream(ctx);
|
|
713
|
+
ANALYSIS_CACHE.set(ctx, a);
|
|
714
|
+
}
|
|
715
|
+
return a;
|
|
716
|
+
}
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// The adapter.
|
|
719
|
+
export const fastStreamAdapter = {
|
|
720
|
+
name: 'faststream',
|
|
721
|
+
async detect(ctx) {
|
|
722
|
+
const { base, rootPath } = resolveBase(ctx);
|
|
723
|
+
// Root scan first (a repo whose manifest is at root reports rootPath '').
|
|
724
|
+
const rootMatch = scoreFastStream(gatherFastStreamSignals(base), rootPath);
|
|
725
|
+
if (rootMatch)
|
|
726
|
+
return rootMatch;
|
|
727
|
+
// A FastStream/Taskiq worker often lives one dir down (a `backend/` | `worker/`
|
|
728
|
+
// | `server/` package in a frontend+backend monorepo), so a root-only scan
|
|
729
|
+
// misses it. Shallow-scan immediate subdirs for a match and scope to it. Only
|
|
730
|
+
// when NOT already scoped to a workspace package (the path).
|
|
731
|
+
if (!ctx.packageDir) {
|
|
732
|
+
for (const sub of shallowManifestSubdirs(base)) {
|
|
733
|
+
const m = scoreFastStream(gatherFastStreamSignals(join(base, sub)), sub);
|
|
734
|
+
if (m)
|
|
735
|
+
return m;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
},
|
|
740
|
+
// One grouping prior per FastStream *Router → its own subsystem, authoritative
|
|
741
|
+
// over directory grouping (the fastapi/flask mechanism). Fully deterministic
|
|
742
|
+
// (var/package-derived) → no classificationsNeeded. A broker-only app (no
|
|
743
|
+
// routers) yields no groups.
|
|
744
|
+
async groupingPrior(ctx) {
|
|
745
|
+
return { groups: getAnalysis(ctx).groups };
|
|
746
|
+
},
|
|
747
|
+
// Pub/sub topic matching (kind 'publishes') + Taskiq kiq enqueue (kind
|
|
748
|
+
// 'publishes') + include_router mounting (kind 'calls'). File-id endpoints; the
|
|
749
|
+
// step resolves to modules, drops self-edges, dedupes, 8-verb-validates.
|
|
750
|
+
async syntheticEdges(ctx) {
|
|
751
|
+
return getAnalysis(ctx).edges;
|
|
752
|
+
},
|
|
753
|
+
// app / broker / router → gateway; subscriber / publisher → service; task → job.
|
|
754
|
+
// METADATA; the module's `kind` is unchanged.
|
|
755
|
+
async roleTags(ctx) {
|
|
756
|
+
return getAnalysis(ctx).roles;
|
|
757
|
+
},
|
|
758
|
+
// The hooks READ SOURCE (Python). Declare the paths the diff-driven hosted walk
|
|
759
|
+
// must treat as framework-relevant. Never-store-source holds: parse server-side,
|
|
760
|
+
// persist only the derived groups/edges/roles.
|
|
761
|
+
scansSourcePath(path) {
|
|
762
|
+
return path.endsWith('.py') || path.endsWith('.pyi');
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
//# sourceMappingURL=faststream.js.map
|