@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,691 @@
|
|
|
1
|
+
// the Workflow-orchestrator FrameworkAdapter. ONE adapter for the
|
|
2
|
+
// four Python workflow/DAG orchestrators, built on a single shared
|
|
3
|
+
// "decorated flow/task/asset" heuristic + per-tool refinement. Follows the
|
|
4
|
+
// FastAPI/Flask template (/997) and reuses the shared Python core
|
|
5
|
+
// (py-ast + parsePythonScope). Install-free + deterministic (a pure
|
|
6
|
+
// syntactic Pyright parse; never executes repo code); persist only the derived
|
|
7
|
+
// groups/edges/roles.
|
|
8
|
+
//
|
|
9
|
+
// The unifying idea: in every one of these tools the UNIT OF WORK is a DECORATED
|
|
10
|
+
// function — @asset (Dagster) / @flow,@task (Prefect) / @dag,@task (Airflow
|
|
11
|
+
// TaskFlow) / @activity.defn,@workflow.run (Temporal). One heuristic tags all of
|
|
12
|
+
// them; a small per-tool table refines the role name + the dependency DSL.
|
|
13
|
+
//
|
|
14
|
+
// * detect() — any of `dagster` / `prefect` / `apache-airflow` /
|
|
15
|
+
// `temporalio`. The metadata carries WHICH tools are
|
|
16
|
+
// present, so the analysis only runs the vocab/edge passes
|
|
17
|
+
// for the tools actually in the repo (mirrors FastAPI's
|
|
18
|
+
// `celery` flag).
|
|
19
|
+
// * roleTags — a decorated flow/task/asset/op/dag/activity/workflow →
|
|
20
|
+
// `job` (the role: own-code triggered by a
|
|
21
|
+
// schedule/queue/event, not a request). METADATA onto the
|
|
22
|
+
// LOCKED MODULE_KINDS enum; never a new kind.
|
|
23
|
+
// * groupingPrior — a `dags/` directory → one subsystem; and every
|
|
24
|
+
// orchestration CONTAINER (Airflow `@dag`, Dagster `@job`,
|
|
25
|
+
// Prefect `@flow`) → a subsystem grouping the definer +
|
|
26
|
+
// the task/asset files it references. Same authoritative-
|
|
27
|
+
// over-directory mechanism the FastAPI router / Nest
|
|
28
|
+
// @Module priors use.
|
|
29
|
+
// * syntheticEdges — the task-dependency wiring the import graph doesn't name
|
|
30
|
+
// as verbs, resolved STATICALLY to `calls` where possible:
|
|
31
|
+
// · Dagster asset deps — an @asset fn PARAMETER named
|
|
32
|
+
// after another asset, or `@asset(deps=[…])`.
|
|
33
|
+
// · Prefect/Dagster/Airflow — a @task/@op called from a
|
|
34
|
+
// @flow/@job/@dag (cross-file, via import bindings).
|
|
35
|
+
// · Airflow `a >> b` / `a << b` / `a.set_downstream(b)`.
|
|
36
|
+
// · Temporal `workflow.execute_activity(fn, …)`.
|
|
37
|
+
// Endpoints in FILE-id space; the contribute-step resolves
|
|
38
|
+
// to modules, drops self-edges (same-file deps collapse),
|
|
39
|
+
// dedupes, 8-verb-validates. Dynamic/unresolvable deps
|
|
40
|
+
// DEGRADE + LOG — no silent caps.
|
|
41
|
+
//
|
|
42
|
+
// Dagster + Prefect + Airflow-TaskFlow are covered solidly; Temporal is
|
|
43
|
+
// best-effort (its workflow is a decorated CLASS — caught via the always-present
|
|
44
|
+
// `@workflow.run` method rather than class-decorator collection).
|
|
45
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
46
|
+
import { join } from 'node:path';
|
|
47
|
+
import { clampConfidence, resolveBase } from '../detect-util.js';
|
|
48
|
+
import { readPythonDeps } from '../../graph/python-manifest.js';
|
|
49
|
+
import { parsePythonScope } from '../python/analyze.js';
|
|
50
|
+
import { callCallee, keywordArg, listItems, memberChain, nameValue, positionalArgs, stringValue, PN, } from '../python/py-ast.js';
|
|
51
|
+
/** Gather the signal set for a single root dir (reads manifests only). */
|
|
52
|
+
export function gatherOrchestratorSignals(baseDir) {
|
|
53
|
+
const deps = readPythonDeps(baseDir);
|
|
54
|
+
return {
|
|
55
|
+
tools: {
|
|
56
|
+
dagster: deps.has('dagster'),
|
|
57
|
+
prefect: deps.has('prefect'),
|
|
58
|
+
// The PyPI distribution is `apache-airflow`; accept a bare `airflow` too.
|
|
59
|
+
airflow: deps.has('apache-airflow') || deps.has('airflow'),
|
|
60
|
+
temporal: deps.has('temporalio'),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function toolCount(t) {
|
|
65
|
+
return (t.dagster ? 1 : 0) + (t.prefect ? 1 : 0) + (t.airflow ? 1 : 0) + (t.temporal ? 1 : 0);
|
|
66
|
+
}
|
|
67
|
+
// Non-source dirs the nested scan skips (cheap + can't hold a first-party manifest).
|
|
68
|
+
const NESTED_SKIP_DIRS = new Set([
|
|
69
|
+
'node_modules',
|
|
70
|
+
'dist',
|
|
71
|
+
'build',
|
|
72
|
+
'out',
|
|
73
|
+
'coverage',
|
|
74
|
+
'venv',
|
|
75
|
+
'.venv',
|
|
76
|
+
'__pycache__',
|
|
77
|
+
'site-packages',
|
|
78
|
+
]);
|
|
79
|
+
/** True if `dir` holds a Python manifest worth scanning for deps. */
|
|
80
|
+
function hasPythonManifest(dir) {
|
|
81
|
+
return (existsSync(join(dir, 'pyproject.toml')) ||
|
|
82
|
+
existsSync(join(dir, 'setup.py')) ||
|
|
83
|
+
existsSync(join(dir, 'setup.cfg')) ||
|
|
84
|
+
existsSync(join(dir, 'requirements.txt')));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Immediate subdirs (depth 1) that contain a Python manifest — the shallow search
|
|
88
|
+
* for a nested orchestration package. Sorted, so the first-match pick is
|
|
89
|
+
* deterministic; skips dot-dirs + non-source dirs to stay cheap.
|
|
90
|
+
*/
|
|
91
|
+
function shallowManifestSubdirs(base) {
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = readdirSync(base, { withFileTypes: true });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const e of entries) {
|
|
101
|
+
if (!e.isDirectory() || e.name.startsWith('.') || NESTED_SKIP_DIRS.has(e.name))
|
|
102
|
+
continue;
|
|
103
|
+
if (hasPythonManifest(join(base, e.name)))
|
|
104
|
+
out.push(e.name);
|
|
105
|
+
}
|
|
106
|
+
return out.sort();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decide the orchestrator match from the signal set. At least one of the four
|
|
110
|
+
* deps is REQUIRED; a second raises confidence (a repo wiring two orchestrators
|
|
111
|
+
* is an even stronger signal). Returns null → generic-Python fallthrough,
|
|
112
|
+
* byte-for-byte unchanged.
|
|
113
|
+
*/
|
|
114
|
+
export function scoreOrchestrator(s, rootPath = '') {
|
|
115
|
+
const n = toolCount(s.tools);
|
|
116
|
+
if (n === 0)
|
|
117
|
+
return null;
|
|
118
|
+
let confidence = 0.8;
|
|
119
|
+
if (n > 1)
|
|
120
|
+
confidence += 0.1;
|
|
121
|
+
return {
|
|
122
|
+
adapter: 'orchestrator',
|
|
123
|
+
confidence: clampConfidence(confidence),
|
|
124
|
+
rootPath,
|
|
125
|
+
metadata: { tools: s.tools },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Collapse priority when one FILE carries several roles (a @dag file that also
|
|
129
|
+
// declares @task functions reads as `dag` — the orchestration headline; a
|
|
130
|
+
// Prefect @flow+@task file reads as `flow`). Higher wins; lexical tiebreak.
|
|
131
|
+
const ROLE_PRIORITY = {
|
|
132
|
+
workflow: 11,
|
|
133
|
+
dag: 10,
|
|
134
|
+
job: 9,
|
|
135
|
+
flow: 8,
|
|
136
|
+
asset: 7,
|
|
137
|
+
op: 6,
|
|
138
|
+
activity: 5,
|
|
139
|
+
task: 4,
|
|
140
|
+
sensor: 3,
|
|
141
|
+
schedule: 2,
|
|
142
|
+
'task-group': 1,
|
|
143
|
+
};
|
|
144
|
+
// Every role maps onto the single locked kind `job` (never a new Module-kind).
|
|
145
|
+
const ROLE_KIND = 'job';
|
|
146
|
+
// Roles that are orchestration CONTAINERS — each becomes its own subsystem
|
|
147
|
+
// grouping the tasks/assets it references (Airflow @dag, Dagster @job, Prefect
|
|
148
|
+
// @flow). Temporal's @workflow is a unit, not a container of other files.
|
|
149
|
+
const CONTAINER_ROLES = new Set(['dag', 'job', 'flow']);
|
|
150
|
+
// Roles that are a "unit" a container can reference cross-file (a call from a
|
|
151
|
+
// @flow/@job/@dag to one of these files becomes a `calls` edge + group member).
|
|
152
|
+
const UNIT_ROLES = new Set(['asset', 'op', 'task', 'flow', 'activity', 'workflow']);
|
|
153
|
+
// Temporal orchestration-call methods whose first positional arg is the
|
|
154
|
+
// activity/child-workflow reference (best-effort).
|
|
155
|
+
const TEMPORAL_CALL_METHODS = new Set([
|
|
156
|
+
'execute_activity',
|
|
157
|
+
'execute_local_activity',
|
|
158
|
+
'execute_activity_method',
|
|
159
|
+
'execute_child_workflow',
|
|
160
|
+
'start_activity',
|
|
161
|
+
'start_child_workflow',
|
|
162
|
+
'start_local_activity',
|
|
163
|
+
]);
|
|
164
|
+
// Airflow imperative dependency setters (a.set_downstream(b) / b.set_upstream(a)).
|
|
165
|
+
const DOWNSTREAM_METHODS = new Set(['set_downstream', 'set_upstream']);
|
|
166
|
+
// Pinned OperatorType values (parser/tokenizerTypes) for the Airflow `>>` / `<<`
|
|
167
|
+
// task-dependency DSL. The dep is pinned EXACTLY (same rationale as py-ast's PN).
|
|
168
|
+
const OP_RSHIFT = 31; // `a >> b` — a upstream of b
|
|
169
|
+
const OP_LSHIFT = 17; // `a << b` — b upstream of a
|
|
170
|
+
/**
|
|
171
|
+
* Classify a decorator's `{root, path}` chain to an orchestrator role, GATED by
|
|
172
|
+
* which tools the repo declares (so a bare `@task` is only claimed when a tool
|
|
173
|
+
* that defines it is present). Matches on the chain LEAF (`@dg.asset` and bare
|
|
174
|
+
* `@asset` both read as leaf `asset`), so it's import-alias-independent.
|
|
175
|
+
*/
|
|
176
|
+
export function classifyDecorator(chain, tools) {
|
|
177
|
+
const leaf = chain.path.length ? chain.path[chain.path.length - 1] : chain.root;
|
|
178
|
+
if (tools.dagster) {
|
|
179
|
+
if (leaf === 'asset' || leaf === 'multi_asset' || leaf === 'graph_asset' || leaf === 'graph_multi_asset' || leaf === 'asset_check')
|
|
180
|
+
return 'asset';
|
|
181
|
+
if (leaf === 'op')
|
|
182
|
+
return 'op';
|
|
183
|
+
if (leaf === 'job' || leaf === 'graph')
|
|
184
|
+
return 'job';
|
|
185
|
+
if (leaf === 'sensor' || leaf === 'asset_sensor' || leaf === 'multi_asset_sensor' || leaf === 'run_status_sensor' || leaf === 'run_failure_sensor')
|
|
186
|
+
return 'sensor';
|
|
187
|
+
if (leaf === 'schedule')
|
|
188
|
+
return 'schedule';
|
|
189
|
+
}
|
|
190
|
+
if (tools.airflow) {
|
|
191
|
+
// The TaskFlow `task` namespace covers `@task` and `@task.<variant>`
|
|
192
|
+
// (virtualenv/docker/kubernetes/branch/short_circuit/…): match the ROOT.
|
|
193
|
+
if (chain.root === 'task')
|
|
194
|
+
return 'task';
|
|
195
|
+
if (leaf === 'dag')
|
|
196
|
+
return 'dag';
|
|
197
|
+
if (leaf === 'task_group')
|
|
198
|
+
return 'task-group';
|
|
199
|
+
}
|
|
200
|
+
if (tools.prefect) {
|
|
201
|
+
if (leaf === 'flow')
|
|
202
|
+
return 'flow';
|
|
203
|
+
if (leaf === 'task')
|
|
204
|
+
return 'task';
|
|
205
|
+
}
|
|
206
|
+
if (tools.temporal) {
|
|
207
|
+
// @workflow.defn (class) / @workflow.run|signal|query|update (method).
|
|
208
|
+
if (chain.root === 'workflow')
|
|
209
|
+
return 'workflow';
|
|
210
|
+
// @activity.defn (function).
|
|
211
|
+
if (chain.root === 'activity' && (chain.path.length === 0 || chain.path[0] === 'defn'))
|
|
212
|
+
return 'activity';
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
// Memoized on the FrameworkContext OBJECT (not repoDir) so groupingPrior +
|
|
217
|
+
// syntheticEdges + roleTags share ONE parse, while the merge walk's per-checkpoint
|
|
218
|
+
// ctx gets a fresh analysis. Mirrors fastapi / flask / nest.
|
|
219
|
+
const ANALYSIS_CACHE = new WeakMap();
|
|
220
|
+
function slugify(s) {
|
|
221
|
+
return s
|
|
222
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
223
|
+
.toLowerCase()
|
|
224
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
225
|
+
.replace(/^-+|-+$/g, '');
|
|
226
|
+
}
|
|
227
|
+
function humanize(s) {
|
|
228
|
+
const words = s
|
|
229
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
230
|
+
.replace(/[_\-.]+/g, ' ')
|
|
231
|
+
.trim()
|
|
232
|
+
.split(/\s+/)
|
|
233
|
+
.filter(Boolean);
|
|
234
|
+
if (words.length === 0)
|
|
235
|
+
return s;
|
|
236
|
+
return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
237
|
+
}
|
|
238
|
+
function baseName(fileId) {
|
|
239
|
+
const last = fileId.split('/').pop() ?? fileId;
|
|
240
|
+
return last.replace(/\.[^.]+$/, '');
|
|
241
|
+
}
|
|
242
|
+
function dirSegment(fileId) {
|
|
243
|
+
const slash = fileId.indexOf('/');
|
|
244
|
+
return slash > 0 ? slugify(fileId.slice(0, slash)) : 'root';
|
|
245
|
+
}
|
|
246
|
+
// Keep the highest-priority candidate role per file (deterministic, lexical
|
|
247
|
+
// tiebreak) — mirrors the fastapi/flask addRole + the contribute-step's collapse.
|
|
248
|
+
function addRole(map, fileId, role) {
|
|
249
|
+
const cur = map.get(fileId);
|
|
250
|
+
if (cur === undefined ||
|
|
251
|
+
ROLE_PRIORITY[role] > ROLE_PRIORITY[cur] ||
|
|
252
|
+
(ROLE_PRIORITY[role] === ROLE_PRIORITY[cur] && role < cur)) {
|
|
253
|
+
map.set(fileId, role);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function addEdge(edges, from, to, relation) {
|
|
257
|
+
if (from === to)
|
|
258
|
+
return; // intra-file deps collapse; the step drops self-edges too
|
|
259
|
+
const key = `${from}→${to}:calls`;
|
|
260
|
+
if (!edges.has(key)) {
|
|
261
|
+
edges.set(key, { source: from, target: to, kind: 'calls', metadata: { framework: 'orchestrator', relation } });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// The constructor name of an `x = Ctor(...)` assignment RHS (last chain segment,
|
|
265
|
+
// so both `DAG(...)` and `airflow.DAG(...)` read as 'DAG'), or undefined.
|
|
266
|
+
function assignedCtorName(rhs) {
|
|
267
|
+
if (rhs.nodeType !== PN.Call)
|
|
268
|
+
return undefined;
|
|
269
|
+
const callee = callCallee(rhs);
|
|
270
|
+
if (!callee)
|
|
271
|
+
return undefined;
|
|
272
|
+
return callee.path.length ? callee.path[callee.path.length - 1] : callee.root;
|
|
273
|
+
}
|
|
274
|
+
// A decorator's callee chain (`@bp.route(...)` → root 'bp', path ['route'];
|
|
275
|
+
// `@shared_task` → root 'shared_task', path []; `@workflow.defn` → root
|
|
276
|
+
// 'workflow', path ['defn']). The decorator expr is either a call or a bare
|
|
277
|
+
// name/attribute. Mirrors the fastapi/flask local helper (per-adapter pattern).
|
|
278
|
+
function decoratorChain(deco) {
|
|
279
|
+
const expr = deco.d.expr;
|
|
280
|
+
if (expr.nodeType === PN.Call)
|
|
281
|
+
return callCallee(expr);
|
|
282
|
+
return memberChain(expr);
|
|
283
|
+
}
|
|
284
|
+
// A container decorator's human label: its `dag_id=`/`name=` string arg when the
|
|
285
|
+
// decorator is called, else the decorated function's name.
|
|
286
|
+
function containerLabel(deco, fnName) {
|
|
287
|
+
const expr = deco.d.expr;
|
|
288
|
+
if (expr.nodeType === PN.Call) {
|
|
289
|
+
const call = expr;
|
|
290
|
+
const s = stringValue(keywordArg(call, 'dag_id')) ?? stringValue(keywordArg(call, 'name'));
|
|
291
|
+
if (s)
|
|
292
|
+
return s;
|
|
293
|
+
}
|
|
294
|
+
return fnName;
|
|
295
|
+
}
|
|
296
|
+
// Resolve a symbol name used at a task-dependency site to a file id: an imported
|
|
297
|
+
// symbol resolves to its module file; anything else (a local task variable, an
|
|
298
|
+
// unresolved name) resolves to THIS file → a self-edge the step drops.
|
|
299
|
+
function resolveName(root, binds, thisFile) {
|
|
300
|
+
return binds.get(root) ?? thisFile;
|
|
301
|
+
}
|
|
302
|
+
// Resolve a task-dependency operand EXPRESSION (a task variable `a`, an
|
|
303
|
+
// attribute `mod.a`, or a task call `extract()`) to a file id, or undefined when
|
|
304
|
+
// it isn't a dotted-name/call we can anchor (a dynamic expression → logged).
|
|
305
|
+
function operandFile(expr, binds, thisFile) {
|
|
306
|
+
const mc = memberChain(expr);
|
|
307
|
+
if (mc)
|
|
308
|
+
return resolveName(mc.root, binds, thisFile);
|
|
309
|
+
if (expr.nodeType === PN.Call) {
|
|
310
|
+
const callee = callCallee(expr);
|
|
311
|
+
if (callee)
|
|
312
|
+
return resolveName(callee.root, binds, thisFile);
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
// The upstream-most (`head`) or downstream-most (`tail`) task operands of a
|
|
317
|
+
// `>>`/`<<` chain, expanding list operands (`[a, b] >> c`) and recursing through
|
|
318
|
+
// nested shifts. For `a >> b`: head = a's head, tail = b's tail; `<<` mirrors.
|
|
319
|
+
function shiftEnds(expr, side) {
|
|
320
|
+
const t = expr.nodeType;
|
|
321
|
+
if (t === PN.List)
|
|
322
|
+
return listItems(expr);
|
|
323
|
+
if (t === PN.BinaryOperation) {
|
|
324
|
+
const b = expr;
|
|
325
|
+
if (b.d.operator === OP_RSHIFT)
|
|
326
|
+
return shiftEnds(side === 'head' ? b.d.leftExpr : b.d.rightExpr, side);
|
|
327
|
+
if (b.d.operator === OP_LSHIFT)
|
|
328
|
+
return shiftEnds(side === 'head' ? b.d.rightExpr : b.d.leftExpr, side);
|
|
329
|
+
return [expr]; // a non-shift operator is an opaque endpoint
|
|
330
|
+
}
|
|
331
|
+
return [expr]; // a Name / MemberAccess / Call leaf
|
|
332
|
+
}
|
|
333
|
+
function assignGroups(seeds) {
|
|
334
|
+
const taken = new Set();
|
|
335
|
+
const ordered = [...seeds].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
336
|
+
const groups = [];
|
|
337
|
+
for (const seed of ordered) {
|
|
338
|
+
let id = seed.baseSlug;
|
|
339
|
+
if (taken.has(id))
|
|
340
|
+
id = `${seed.baseSlug}-${dirSegment(seed.key)}`;
|
|
341
|
+
let n = 2;
|
|
342
|
+
while (taken.has(id))
|
|
343
|
+
id = `${seed.baseSlug}-${n++}`;
|
|
344
|
+
taken.add(id);
|
|
345
|
+
groups.push({ id, label: seed.label, fileIds: [...seed.fileIds].sort() });
|
|
346
|
+
}
|
|
347
|
+
return groups.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
348
|
+
}
|
|
349
|
+
// The `dags/` directory path prefix a file lives under (up to & including the
|
|
350
|
+
// `dags` segment), or undefined. `svc/dags/etl.py` → `svc/dags`.
|
|
351
|
+
function dagsDirOf(fileId) {
|
|
352
|
+
const parts = fileId.split('/');
|
|
353
|
+
const idx = parts.findIndex((p) => p.toLowerCase() === 'dags');
|
|
354
|
+
if (idx < 0 || idx === parts.length - 1)
|
|
355
|
+
return undefined; // not under a dags/ dir
|
|
356
|
+
return parts.slice(0, idx + 1).join('/');
|
|
357
|
+
}
|
|
358
|
+
function analyzeOrchestrator(ctx) {
|
|
359
|
+
const { parsed } = parsePythonScope(ctx);
|
|
360
|
+
const tools = ctx.match.metadata?.tools ?? {
|
|
361
|
+
dagster: false,
|
|
362
|
+
prefect: false,
|
|
363
|
+
airflow: false,
|
|
364
|
+
temporal: false,
|
|
365
|
+
};
|
|
366
|
+
const roleByFile = new Map();
|
|
367
|
+
const diag = {
|
|
368
|
+
ambiguousAssets: new Set(),
|
|
369
|
+
unresolvedShift: new Set(),
|
|
370
|
+
unresolvedTemporal: new Set(),
|
|
371
|
+
};
|
|
372
|
+
// Pass 1 — roles + indexes (asset-name → file(s); the set of unit files; the
|
|
373
|
+
// container files; each asset def's params/deps for the Dagster dependency pass).
|
|
374
|
+
const assetFilesByName = new Map(); // asset fn name → defining file(s)
|
|
375
|
+
const unitFiles = new Set(); // files that define any orchestrator unit
|
|
376
|
+
const containerByFile = new Map(); // container file → its label/role
|
|
377
|
+
const assetDefs = [];
|
|
378
|
+
for (const [id, file] of parsed) {
|
|
379
|
+
// Classic Airflow `dag = DAG(...)` (the non-decorator, context-manager-adjacent
|
|
380
|
+
// form) — its file is a DAG orchestration unit + container.
|
|
381
|
+
if (tools.airflow) {
|
|
382
|
+
for (const a of file.nodes.assignments) {
|
|
383
|
+
if (assignedCtorName(a.d.rightExpr) !== 'DAG')
|
|
384
|
+
continue;
|
|
385
|
+
addRole(roleByFile, id, 'dag');
|
|
386
|
+
if (!containerByFile.has(id)) {
|
|
387
|
+
const call = a.d.rightExpr;
|
|
388
|
+
const label = stringValue(keywordArg(call, 'dag_id')) ??
|
|
389
|
+
stringValue(positionalArgs(call)[0]) ??
|
|
390
|
+
nameValue(a.d.leftExpr) ??
|
|
391
|
+
baseName(id);
|
|
392
|
+
containerByFile.set(id, { role: 'dag', label });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const fn of file.nodes.functions) {
|
|
397
|
+
const fnName = fn.d.name.d.value;
|
|
398
|
+
for (const deco of fn.d.decorators) {
|
|
399
|
+
const chain = decoratorChain(deco);
|
|
400
|
+
if (!chain)
|
|
401
|
+
continue;
|
|
402
|
+
const role = classifyDecorator(chain, tools);
|
|
403
|
+
if (!role)
|
|
404
|
+
continue;
|
|
405
|
+
addRole(roleByFile, id, role);
|
|
406
|
+
if (UNIT_ROLES.has(role))
|
|
407
|
+
unitFiles.add(id);
|
|
408
|
+
if (role === 'asset') {
|
|
409
|
+
let set = assetFilesByName.get(fnName);
|
|
410
|
+
if (!set) {
|
|
411
|
+
set = new Set();
|
|
412
|
+
assetFilesByName.set(fnName, set);
|
|
413
|
+
}
|
|
414
|
+
set.add(id);
|
|
415
|
+
const call = deco.d.expr.nodeType === PN.Call ? deco.d.expr : undefined;
|
|
416
|
+
const params = fn.d.params.map((p) => p.d.name?.d.value).filter((n) => !!n);
|
|
417
|
+
const depsItems = call ? listItems(keywordArg(call, 'deps')) : [];
|
|
418
|
+
assetDefs.push({ fileId: id, params, depsItems });
|
|
419
|
+
}
|
|
420
|
+
if (CONTAINER_ROLES.has(role) && !containerByFile.has(id)) {
|
|
421
|
+
containerByFile.set(id, { role, label: containerLabel(deco, fnName) });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Resolve an asset KEY (a param name, a `deps=` string key) to its defining
|
|
427
|
+
// file via the global asset-name index — Dagster wires assets by key, not by
|
|
428
|
+
// import. Ambiguous (same name in >1 file) → undefined + logged.
|
|
429
|
+
function assetByName(name) {
|
|
430
|
+
const set = assetFilesByName.get(name);
|
|
431
|
+
if (!set || set.size === 0)
|
|
432
|
+
return undefined;
|
|
433
|
+
if (set.size > 1) {
|
|
434
|
+
diag.ambiguousAssets.add(`${name} → {${[...set].sort().join(', ')}}`);
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
return [...set][0];
|
|
438
|
+
}
|
|
439
|
+
// Pass 2 — edges + container group membership.
|
|
440
|
+
const edges = new Map();
|
|
441
|
+
const containerMembers = new Map(); // container file → member files
|
|
442
|
+
for (const id of containerByFile.keys())
|
|
443
|
+
containerMembers.set(id, new Set([id]));
|
|
444
|
+
// Dagster asset deps — an @asset PARAMETER named after an asset (resolved by
|
|
445
|
+
// asset key), or an `@asset(deps=[…])` reference (an imported asset fn → its
|
|
446
|
+
// import binding; a string key → the asset index). Direction: upstream → this.
|
|
447
|
+
for (const def of assetDefs) {
|
|
448
|
+
const binds = parsed.get(def.fileId).bindings;
|
|
449
|
+
for (const paramName of def.params) {
|
|
450
|
+
const upstream = assetByName(paramName);
|
|
451
|
+
if (upstream)
|
|
452
|
+
addEdge(edges, upstream, def.fileId, 'asset-dep-param');
|
|
453
|
+
}
|
|
454
|
+
for (const item of def.depsItems) {
|
|
455
|
+
let upstream;
|
|
456
|
+
const mc = memberChain(item);
|
|
457
|
+
if (mc)
|
|
458
|
+
upstream = binds.get(mc.root) ?? assetByName(mc.root);
|
|
459
|
+
else {
|
|
460
|
+
const key = stringValue(item);
|
|
461
|
+
if (key)
|
|
462
|
+
upstream = assetByName(key.split('/').pop() ?? key);
|
|
463
|
+
}
|
|
464
|
+
if (upstream)
|
|
465
|
+
addEdge(edges, upstream, def.fileId, 'asset-dep');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
for (const [id, file] of parsed) {
|
|
469
|
+
const binds = file.bindings;
|
|
470
|
+
const isContainer = containerByFile.has(id);
|
|
471
|
+
// Container → imported-unit calls (Prefect @flow → @task, Dagster @job → @op,
|
|
472
|
+
// Airflow @dag → an imported @task). Same-file tasks aren't imported → no
|
|
473
|
+
// binding → no edge (they collapse anyway). Each target also joins the group.
|
|
474
|
+
if (isContainer) {
|
|
475
|
+
for (const call of file.nodes.calls) {
|
|
476
|
+
const callee = callCallee(call);
|
|
477
|
+
if (!callee)
|
|
478
|
+
continue;
|
|
479
|
+
const target = binds.get(callee.root);
|
|
480
|
+
if (target && target !== id && unitFiles.has(target)) {
|
|
481
|
+
addEdge(edges, id, target, 'contains-task');
|
|
482
|
+
containerMembers.get(id).add(target);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Temporal orchestration calls (best-effort): the first positional arg of
|
|
487
|
+
// `workflow.execute_activity(fn, …)` / `execute_child_workflow(Wf, …)` is the
|
|
488
|
+
// activity/child-workflow reference.
|
|
489
|
+
if (tools.temporal) {
|
|
490
|
+
for (const call of file.nodes.calls) {
|
|
491
|
+
const callee = callCallee(call);
|
|
492
|
+
if (!callee || callee.path.length === 0)
|
|
493
|
+
continue;
|
|
494
|
+
if (!TEMPORAL_CALL_METHODS.has(callee.path[callee.path.length - 1]))
|
|
495
|
+
continue;
|
|
496
|
+
const arg = positionalArgs(call)[0];
|
|
497
|
+
if (!arg)
|
|
498
|
+
continue;
|
|
499
|
+
const target = operandFile(arg, binds, id);
|
|
500
|
+
if (target && target !== id)
|
|
501
|
+
addEdge(edges, id, target, 'temporal-execute');
|
|
502
|
+
else if (!target)
|
|
503
|
+
diag.unresolvedTemporal.add(`${id}: ${callee.path.join('.')}(…)`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Airflow imperative dependency setters + the `>>`/`<<` DSL.
|
|
507
|
+
if (tools.airflow) {
|
|
508
|
+
for (const call of file.nodes.calls) {
|
|
509
|
+
const callee = callCallee(call);
|
|
510
|
+
if (!callee || callee.path.length !== 1 || !DOWNSTREAM_METHODS.has(callee.path[0]))
|
|
511
|
+
continue;
|
|
512
|
+
const arg = positionalArgs(call)[0];
|
|
513
|
+
if (!arg)
|
|
514
|
+
continue;
|
|
515
|
+
const obj = resolveName(callee.root, binds, id);
|
|
516
|
+
const other = operandFile(arg, binds, id);
|
|
517
|
+
if (!other) {
|
|
518
|
+
diag.unresolvedShift.add(`${id}: ${callee.root}.${callee.path[0]}(…)`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
// set_downstream: obj → other; set_upstream: other → obj.
|
|
522
|
+
if (callee.path[0] === 'set_downstream')
|
|
523
|
+
addEdge(edges, obj, other, 'set-downstream');
|
|
524
|
+
else
|
|
525
|
+
addEdge(edges, other, obj, 'set-upstream');
|
|
526
|
+
}
|
|
527
|
+
for (const b of file.nodes.binaryOps) {
|
|
528
|
+
const rs = b.d.operator === OP_RSHIFT;
|
|
529
|
+
const ls = b.d.operator === OP_LSHIFT;
|
|
530
|
+
if (!rs && !ls)
|
|
531
|
+
continue;
|
|
532
|
+
// Upstream set → downstream set. RS `L >> R`: tails(L) → heads(R);
|
|
533
|
+
// LS `L << R` (R upstream of L): tails(R) → heads(L).
|
|
534
|
+
const ups = rs ? shiftEnds(b.d.leftExpr, 'tail') : shiftEnds(b.d.rightExpr, 'tail');
|
|
535
|
+
const downs = rs ? shiftEnds(b.d.rightExpr, 'head') : shiftEnds(b.d.leftExpr, 'head');
|
|
536
|
+
for (const u of ups) {
|
|
537
|
+
const uf = operandFile(u, binds, id);
|
|
538
|
+
if (!uf) {
|
|
539
|
+
diag.unresolvedShift.add(`${id}: dynamic shift operand`);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
for (const d of downs) {
|
|
543
|
+
const df = operandFile(d, binds, id);
|
|
544
|
+
if (!df) {
|
|
545
|
+
diag.unresolvedShift.add(`${id}: dynamic shift operand`);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
addEdge(edges, uf, df, 'shift-dep');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Grouping — a `dags/` dir group + a per-container group. A `@dag`/`@job`/
|
|
555
|
+
// `@flow` DEFINER is excluded from the coarse dags-dir group so its finer,
|
|
556
|
+
// semantic per-container subsystem always wins; the dags-dir group then catches
|
|
557
|
+
// the rest (classic `with DAG(...)` files, shared helpers, configs) that have
|
|
558
|
+
// no container of their own.
|
|
559
|
+
const seeds = [];
|
|
560
|
+
const dagsDirs = new Map();
|
|
561
|
+
for (const id of parsed.keys()) {
|
|
562
|
+
if (containerByFile.has(id))
|
|
563
|
+
continue; // its own per-container group wins
|
|
564
|
+
const dir = dagsDirOf(id);
|
|
565
|
+
if (!dir)
|
|
566
|
+
continue;
|
|
567
|
+
let set = dagsDirs.get(dir);
|
|
568
|
+
if (!set) {
|
|
569
|
+
set = new Set();
|
|
570
|
+
dagsDirs.set(dir, set);
|
|
571
|
+
}
|
|
572
|
+
set.add(id);
|
|
573
|
+
}
|
|
574
|
+
for (const [dir, fileIds] of dagsDirs) {
|
|
575
|
+
// Label the group by its parent dir when nested (`svc/dags` → "Svc DAGs"),
|
|
576
|
+
// else just "DAGs". Slug from the full dir path so multiple dags/ dirs never
|
|
577
|
+
// collide.
|
|
578
|
+
const parts = dir.split('/');
|
|
579
|
+
const parent = parts.length >= 2 ? humanize(parts[parts.length - 2]) : '';
|
|
580
|
+
seeds.push({
|
|
581
|
+
key: `dagsdir:${dir}`,
|
|
582
|
+
baseSlug: slugify(dir) || 'dags',
|
|
583
|
+
label: parent ? `${parent} DAGs` : 'DAGs',
|
|
584
|
+
fileIds,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
for (const [id, container] of containerByFile) {
|
|
588
|
+
seeds.push({
|
|
589
|
+
key: `container:${id}`,
|
|
590
|
+
baseSlug: slugify(container.label) || 'workflow',
|
|
591
|
+
label: humanize(container.label) || container.label,
|
|
592
|
+
fileIds: containerMembers.get(id),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const groups = assignGroups(seeds);
|
|
596
|
+
// Materialize the role tags.
|
|
597
|
+
const roles = new Map();
|
|
598
|
+
for (const [fileId, role] of roleByFile) {
|
|
599
|
+
roles.set(fileId, {
|
|
600
|
+
role,
|
|
601
|
+
kind: ROLE_KIND,
|
|
602
|
+
priority: ROLE_PRIORITY[role],
|
|
603
|
+
metadata: { framework: 'orchestrator' },
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const sortedEdges = [...edges.values()].sort((a, b) => a.source < b.source
|
|
607
|
+
? -1
|
|
608
|
+
: a.source > b.source
|
|
609
|
+
? 1
|
|
610
|
+
: a.target < b.target
|
|
611
|
+
? -1
|
|
612
|
+
: a.target > b.target
|
|
613
|
+
? 1
|
|
614
|
+
: 0);
|
|
615
|
+
// Positive signal for validation (mirrors fastapi/flask's log line).
|
|
616
|
+
if (groups.length > 0 || roleByFile.size > 0 || sortedEdges.length > 0) {
|
|
617
|
+
const present = Object.keys(tools).filter((k) => tools[k]);
|
|
618
|
+
console.log(` [orchestrator] ${present.join('+') || 'none'}: ${roleByFile.size} role(s) · ${groups.length} group(s) · ${sortedEdges.length} edge(s)`);
|
|
619
|
+
}
|
|
620
|
+
// No silent caps (locked): log everything that degraded.
|
|
621
|
+
const degraded = [];
|
|
622
|
+
if (diag.ambiguousAssets.size > 0) {
|
|
623
|
+
degraded.push(`${diag.ambiguousAssets.size} ambiguous asset name(s): ${[...diag.ambiguousAssets].sort().slice(0, 10).join(' · ')}`);
|
|
624
|
+
}
|
|
625
|
+
if (diag.unresolvedShift.size > 0) {
|
|
626
|
+
degraded.push(`${diag.unresolvedShift.size} dynamic/unresolved task dependency(ies): ${[...diag.unresolvedShift].sort().slice(0, 10).join(' · ')}`);
|
|
627
|
+
}
|
|
628
|
+
if (diag.unresolvedTemporal.size > 0) {
|
|
629
|
+
degraded.push(`${diag.unresolvedTemporal.size} unresolved Temporal call(s): ${[...diag.unresolvedTemporal].sort().slice(0, 10).join(' · ')}`);
|
|
630
|
+
}
|
|
631
|
+
if (degraded.length > 0) {
|
|
632
|
+
console.log(` [orchestrator] degraded: ${degraded.join(' · ')} (logged, not silently dropped)`);
|
|
633
|
+
}
|
|
634
|
+
return { groups, edges: sortedEdges, roles };
|
|
635
|
+
}
|
|
636
|
+
function getAnalysis(ctx) {
|
|
637
|
+
let a = ANALYSIS_CACHE.get(ctx);
|
|
638
|
+
if (!a) {
|
|
639
|
+
a = analyzeOrchestrator(ctx);
|
|
640
|
+
ANALYSIS_CACHE.set(ctx, a);
|
|
641
|
+
}
|
|
642
|
+
return a;
|
|
643
|
+
}
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
// The adapter.
|
|
646
|
+
export const orchestratorAdapter = {
|
|
647
|
+
name: 'orchestrator',
|
|
648
|
+
async detect(ctx) {
|
|
649
|
+
const { base, rootPath } = resolveBase(ctx);
|
|
650
|
+
// Root scan first (a repo whose manifest is at root reports rootPath '').
|
|
651
|
+
const rootMatch = scoreOrchestrator(gatherOrchestratorSignals(base), rootPath);
|
|
652
|
+
if (rootMatch)
|
|
653
|
+
return rootMatch;
|
|
654
|
+
// The orchestration package often lives one dir down (a `pipelines/` |
|
|
655
|
+
// `dagster_project/` | `airflow/` package in a larger repo). Shallow-scan
|
|
656
|
+
// immediate subdirs for a manifest and scope to it. Only when NOT already
|
|
657
|
+
// scoped to a workspace package (the path).
|
|
658
|
+
if (!ctx.packageDir) {
|
|
659
|
+
for (const sub of shallowManifestSubdirs(base)) {
|
|
660
|
+
const m = scoreOrchestrator(gatherOrchestratorSignals(join(base, sub)), sub);
|
|
661
|
+
if (m)
|
|
662
|
+
return m;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
},
|
|
667
|
+
// A `dags/` dir + each orchestration container (@dag/@job/@flow) → its own
|
|
668
|
+
// subsystem, authoritative over directory grouping. Fully deterministic
|
|
669
|
+
// (name/path-derived) → no classificationsNeeded.
|
|
670
|
+
async groupingPrior(ctx) {
|
|
671
|
+
return { groups: getAnalysis(ctx).groups };
|
|
672
|
+
},
|
|
673
|
+
// Task-dependency wiring → `calls` (statically resolvable ones; dynamic ones
|
|
674
|
+
// degrade + log). File-id endpoints; the step resolves to modules, drops
|
|
675
|
+
// self-edges, dedupes, 8-verb-validates.
|
|
676
|
+
async syntheticEdges(ctx) {
|
|
677
|
+
return getAnalysis(ctx).edges;
|
|
678
|
+
},
|
|
679
|
+
// Every decorated flow/task/asset/op/dag/activity/workflow → `job`. METADATA;
|
|
680
|
+
// the module's `kind` is unchanged.
|
|
681
|
+
async roleTags(ctx) {
|
|
682
|
+
return getAnalysis(ctx).roles;
|
|
683
|
+
},
|
|
684
|
+
// The hooks READ SOURCE (Python). Declare the paths the diff-driven hosted walk
|
|
685
|
+
// must treat as framework-relevant. Never-store-source holds: parse server-side,
|
|
686
|
+
// persist only the derived groups/edges/roles.
|
|
687
|
+
scansSourcePath(path) {
|
|
688
|
+
return path.endsWith('.py') || path.endsWith('.pyi');
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
//# sourceMappingURL=orchestrator.js.map
|