@synergenius/flow-weaver 0.2.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 +122 -0
- package/README.md +315 -0
- package/dist/annotation-generator.d.ts +45 -0
- package/dist/annotation-generator.js +557 -0
- package/dist/api/builder.d.ts +223 -0
- package/dist/api/builder.js +345 -0
- package/dist/api/compile.d.ts +92 -0
- package/dist/api/compile.js +149 -0
- package/dist/api/extract-types.d.ts +29 -0
- package/dist/api/extract-types.js +57 -0
- package/dist/api/generate-in-place.d.ts +73 -0
- package/dist/api/generate-in-place.js +1353 -0
- package/dist/api/generate.d.ts +83 -0
- package/dist/api/generate.js +510 -0
- package/dist/api/helpers.d.ts +248 -0
- package/dist/api/helpers.js +285 -0
- package/dist/api/index.d.ts +46 -0
- package/dist/api/index.js +45 -0
- package/dist/api/inline-runtime.d.ts +27 -0
- package/dist/api/inline-runtime.js +551 -0
- package/dist/api/manipulation/connections.d.ts +79 -0
- package/dist/api/manipulation/connections.js +151 -0
- package/dist/api/manipulation/index.d.ts +34 -0
- package/dist/api/manipulation/index.js +41 -0
- package/dist/api/manipulation/node-types.d.ts +123 -0
- package/dist/api/manipulation/node-types.js +200 -0
- package/dist/api/manipulation/nodes.d.ts +144 -0
- package/dist/api/manipulation/nodes.js +333 -0
- package/dist/api/manipulation/ports.d.ts +59 -0
- package/dist/api/manipulation/ports.js +228 -0
- package/dist/api/manipulation/scopes.d.ts +52 -0
- package/dist/api/manipulation/scopes.js +156 -0
- package/dist/api/manipulation/validation.d.ts +6 -0
- package/dist/api/manipulation/validation.js +6 -0
- package/dist/api/manipulation/workflow.d.ts +81 -0
- package/dist/api/manipulation/workflow.js +116 -0
- package/dist/api/manipulation.d.ts +8 -0
- package/dist/api/manipulation.js +8 -0
- package/dist/api/parse.d.ts +48 -0
- package/dist/api/parse.js +110 -0
- package/dist/api/patterns.d.ts +112 -0
- package/dist/api/patterns.js +306 -0
- package/dist/api/query.d.ts +429 -0
- package/dist/api/query.js +816 -0
- package/dist/api/templates.d.ts +98 -0
- package/dist/api/templates.js +117 -0
- package/dist/api/transform.d.ts +31 -0
- package/dist/api/transform.js +40 -0
- package/dist/api/validate.d.ts +25 -0
- package/dist/api/validate.js +39 -0
- package/dist/api/workflow-file-operations.d.ts +29 -0
- package/dist/api/workflow-file-operations.js +180 -0
- package/dist/ast/builder.d.ts +210 -0
- package/dist/ast/builder.js +395 -0
- package/dist/ast/index.d.ts +5 -0
- package/dist/ast/index.js +5 -0
- package/dist/ast/serialization-node.d.ts +6 -0
- package/dist/ast/serialization-node.js +30 -0
- package/dist/ast/serialization.d.ts +43 -0
- package/dist/ast/serialization.js +134 -0
- package/dist/ast/types.d.ts +852 -0
- package/dist/ast/types.js +2 -0
- package/dist/ast/workflow-utils.d.ts +54 -0
- package/dist/ast/workflow-utils.js +114 -0
- package/dist/body-generator.d.ts +31 -0
- package/dist/body-generator.js +35 -0
- package/dist/built-in-nodes/delay.d.ts +11 -0
- package/dist/built-in-nodes/delay.js +29 -0
- package/dist/built-in-nodes/index.d.ts +5 -0
- package/dist/built-in-nodes/index.js +4 -0
- package/dist/built-in-nodes/invoke-workflow.d.ts +13 -0
- package/dist/built-in-nodes/invoke-workflow.js +25 -0
- package/dist/built-in-nodes/mock-types.d.ts +18 -0
- package/dist/built-in-nodes/mock-types.js +12 -0
- package/dist/built-in-nodes/wait-for-event.d.ts +13 -0
- package/dist/built-in-nodes/wait-for-event.js +25 -0
- package/dist/chevrotain-parser/connect-parser.d.ts +24 -0
- package/dist/chevrotain-parser/connect-parser.js +98 -0
- package/dist/chevrotain-parser/grammar-diagrams.d.ts +29 -0
- package/dist/chevrotain-parser/grammar-diagrams.js +264 -0
- package/dist/chevrotain-parser/index.d.ts +25 -0
- package/dist/chevrotain-parser/index.js +27 -0
- package/dist/chevrotain-parser/map-parser.d.ts +33 -0
- package/dist/chevrotain-parser/map-parser.js +130 -0
- package/dist/chevrotain-parser/node-parser.d.ts +36 -0
- package/dist/chevrotain-parser/node-parser.js +466 -0
- package/dist/chevrotain-parser/path-parser.d.ts +28 -0
- package/dist/chevrotain-parser/path-parser.js +118 -0
- package/dist/chevrotain-parser/port-parser.d.ts +36 -0
- package/dist/chevrotain-parser/port-parser.js +442 -0
- package/dist/chevrotain-parser/position-parser.d.ts +20 -0
- package/dist/chevrotain-parser/position-parser.js +83 -0
- package/dist/chevrotain-parser/scope-parser.d.ts +19 -0
- package/dist/chevrotain-parser/scope-parser.js +104 -0
- package/dist/chevrotain-parser/tokens.d.ts +78 -0
- package/dist/chevrotain-parser/tokens.js +384 -0
- package/dist/chevrotain-parser/trigger-cancel-parser.d.ts +50 -0
- package/dist/chevrotain-parser/trigger-cancel-parser.js +282 -0
- package/dist/cli/commands/changelog.d.ts +13 -0
- package/dist/cli/commands/changelog.js +135 -0
- package/dist/cli/commands/compile.d.ts +64 -0
- package/dist/cli/commands/compile.js +278 -0
- package/dist/cli/commands/create.d.ts +33 -0
- package/dist/cli/commands/create.js +147 -0
- package/dist/cli/commands/describe.d.ts +68 -0
- package/dist/cli/commands/describe.js +377 -0
- package/dist/cli/commands/dev.d.ts +32 -0
- package/dist/cli/commands/dev.js +384 -0
- package/dist/cli/commands/diagram.d.ts +13 -0
- package/dist/cli/commands/diagram.js +33 -0
- package/dist/cli/commands/diff.d.ts +11 -0
- package/dist/cli/commands/diff.js +59 -0
- package/dist/cli/commands/doctor.d.ts +57 -0
- package/dist/cli/commands/doctor.js +719 -0
- package/dist/cli/commands/export.d.ts +57 -0
- package/dist/cli/commands/export.js +163 -0
- package/dist/cli/commands/grammar.d.ts +9 -0
- package/dist/cli/commands/grammar.js +39 -0
- package/dist/cli/commands/init.d.ts +59 -0
- package/dist/cli/commands/init.js +435 -0
- package/dist/cli/commands/listen.d.ts +16 -0
- package/dist/cli/commands/listen.js +39 -0
- package/dist/cli/commands/market.d.ts +52 -0
- package/dist/cli/commands/market.js +436 -0
- package/dist/cli/commands/migrate.d.ts +13 -0
- package/dist/cli/commands/migrate.js +89 -0
- package/dist/cli/commands/openapi.d.ts +37 -0
- package/dist/cli/commands/openapi.js +67 -0
- package/dist/cli/commands/pattern.d.ts +34 -0
- package/dist/cli/commands/pattern.js +185 -0
- package/dist/cli/commands/plugin.d.ts +16 -0
- package/dist/cli/commands/plugin.js +176 -0
- package/dist/cli/commands/run.d.ts +49 -0
- package/dist/cli/commands/run.js +191 -0
- package/dist/cli/commands/serve.d.ts +45 -0
- package/dist/cli/commands/serve.js +81 -0
- package/dist/cli/commands/templates.d.ts +8 -0
- package/dist/cli/commands/templates.js +54 -0
- package/dist/cli/commands/ui.d.ts +16 -0
- package/dist/cli/commands/ui.js +130 -0
- package/dist/cli/commands/validate.d.ts +12 -0
- package/dist/cli/commands/validate.js +247 -0
- package/dist/cli/commands/watch.d.ts +9 -0
- package/dist/cli/commands/watch.js +70 -0
- package/dist/cli/flow-weaver.mjs +92924 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +742 -0
- package/dist/cli/templates/ai/mock-provider.d.ts +7 -0
- package/dist/cli/templates/ai/mock-provider.js +64 -0
- package/dist/cli/templates/ai/types.d.ts +47 -0
- package/dist/cli/templates/ai/types.js +5 -0
- package/dist/cli/templates/approvals/index.d.ts +15 -0
- package/dist/cli/templates/approvals/index.js +241 -0
- package/dist/cli/templates/index.d.ts +102 -0
- package/dist/cli/templates/index.js +101 -0
- package/dist/cli/templates/nodes/agent-router.d.ts +3 -0
- package/dist/cli/templates/nodes/agent-router.js +114 -0
- package/dist/cli/templates/nodes/aggregator.d.ts +7 -0
- package/dist/cli/templates/nodes/aggregator.js +63 -0
- package/dist/cli/templates/nodes/conversation-memory.d.ts +3 -0
- package/dist/cli/templates/nodes/conversation-memory.js +85 -0
- package/dist/cli/templates/nodes/http.d.ts +7 -0
- package/dist/cli/templates/nodes/http.js +80 -0
- package/dist/cli/templates/nodes/human-approval.d.ts +3 -0
- package/dist/cli/templates/nodes/human-approval.js +110 -0
- package/dist/cli/templates/nodes/json-extractor.d.ts +3 -0
- package/dist/cli/templates/nodes/json-extractor.js +119 -0
- package/dist/cli/templates/nodes/llm-call.d.ts +3 -0
- package/dist/cli/templates/nodes/llm-call.js +106 -0
- package/dist/cli/templates/nodes/prompt-template.d.ts +3 -0
- package/dist/cli/templates/nodes/prompt-template.js +52 -0
- package/dist/cli/templates/nodes/rag-retriever.d.ts +3 -0
- package/dist/cli/templates/nodes/rag-retriever.js +128 -0
- package/dist/cli/templates/nodes/tool-executor.d.ts +3 -0
- package/dist/cli/templates/nodes/tool-executor.js +108 -0
- package/dist/cli/templates/nodes/transformer.d.ts +7 -0
- package/dist/cli/templates/nodes/transformer.js +68 -0
- package/dist/cli/templates/nodes/validator.d.ts +7 -0
- package/dist/cli/templates/nodes/validator.js +62 -0
- package/dist/cli/templates/providers/index.d.ts +14 -0
- package/dist/cli/templates/providers/index.js +239 -0
- package/dist/cli/templates/shared/approval-types.d.ts +9 -0
- package/dist/cli/templates/shared/approval-types.js +31 -0
- package/dist/cli/templates/shared/llm-types.d.ts +15 -0
- package/dist/cli/templates/shared/llm-types.js +104 -0
- package/dist/cli/templates/workflows/aggregator.d.ts +7 -0
- package/dist/cli/templates/workflows/aggregator.js +104 -0
- package/dist/cli/templates/workflows/ai-agent-durable.d.ts +8 -0
- package/dist/cli/templates/workflows/ai-agent-durable.js +338 -0
- package/dist/cli/templates/workflows/ai-agent.d.ts +31 -0
- package/dist/cli/templates/workflows/ai-agent.js +326 -0
- package/dist/cli/templates/workflows/ai-chat.d.ts +7 -0
- package/dist/cli/templates/workflows/ai-chat.js +169 -0
- package/dist/cli/templates/workflows/ai-pipeline-durable.d.ts +8 -0
- package/dist/cli/templates/workflows/ai-pipeline-durable.js +330 -0
- package/dist/cli/templates/workflows/ai-rag.d.ts +7 -0
- package/dist/cli/templates/workflows/ai-rag.js +186 -0
- package/dist/cli/templates/workflows/ai-react.d.ts +7 -0
- package/dist/cli/templates/workflows/ai-react.js +294 -0
- package/dist/cli/templates/workflows/conditional.d.ts +12 -0
- package/dist/cli/templates/workflows/conditional.js +142 -0
- package/dist/cli/templates/workflows/error-handler.d.ts +7 -0
- package/dist/cli/templates/workflows/error-handler.js +147 -0
- package/dist/cli/templates/workflows/foreach.d.ts +7 -0
- package/dist/cli/templates/workflows/foreach.js +143 -0
- package/dist/cli/templates/workflows/sequential.d.ts +7 -0
- package/dist/cli/templates/workflows/sequential.js +198 -0
- package/dist/cli/templates/workflows/webhook.d.ts +7 -0
- package/dist/cli/templates/workflows/webhook.js +161 -0
- package/dist/cli/utils/logger.d.ts +15 -0
- package/dist/cli/utils/logger.js +46 -0
- package/dist/constants.d.ts +100 -0
- package/dist/constants.js +125 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.js +3 -0
- package/dist/deployment/config/defaults.d.ts +29 -0
- package/dist/deployment/config/defaults.js +98 -0
- package/dist/deployment/config/loader.d.ts +24 -0
- package/dist/deployment/config/loader.js +236 -0
- package/dist/deployment/config/types.d.ts +117 -0
- package/dist/deployment/config/types.js +5 -0
- package/dist/deployment/core/adapters.d.ts +90 -0
- package/dist/deployment/core/adapters.js +251 -0
- package/dist/deployment/core/executor.d.ts +62 -0
- package/dist/deployment/core/executor.js +197 -0
- package/dist/deployment/core/formatters.d.ts +57 -0
- package/dist/deployment/core/formatters.js +170 -0
- package/dist/deployment/index.d.ts +31 -0
- package/dist/deployment/index.js +48 -0
- package/dist/deployment/openapi/generator.d.ts +146 -0
- package/dist/deployment/openapi/generator.js +347 -0
- package/dist/deployment/openapi/schema-converter.d.ts +49 -0
- package/dist/deployment/openapi/schema-converter.js +192 -0
- package/dist/deployment/targets/base.d.ts +316 -0
- package/dist/deployment/targets/base.js +823 -0
- package/dist/deployment/targets/cloudflare.d.ts +23 -0
- package/dist/deployment/targets/cloudflare.js +1125 -0
- package/dist/deployment/targets/inngest.d.ts +38 -0
- package/dist/deployment/targets/inngest.js +926 -0
- package/dist/deployment/targets/lambda.d.ts +23 -0
- package/dist/deployment/targets/lambda.js +1289 -0
- package/dist/deployment/targets/vercel.d.ts +23 -0
- package/dist/deployment/targets/vercel.js +886 -0
- package/dist/deployment/types.d.ts +183 -0
- package/dist/deployment/types.js +8 -0
- package/dist/diagram/geometry.d.ts +26 -0
- package/dist/diagram/geometry.js +850 -0
- package/dist/diagram/index.d.ts +16 -0
- package/dist/diagram/index.js +42 -0
- package/dist/diagram/layout.d.ts +11 -0
- package/dist/diagram/layout.js +143 -0
- package/dist/diagram/orthogonal-router.d.ts +79 -0
- package/dist/diagram/orthogonal-router.js +568 -0
- package/dist/diagram/renderer.d.ts +3 -0
- package/dist/diagram/renderer.js +207 -0
- package/dist/diagram/theme.d.ts +20 -0
- package/dist/diagram/theme.js +189 -0
- package/dist/diagram/types.d.ts +70 -0
- package/dist/diagram/types.js +2 -0
- package/dist/diff/WorkflowDiffer.d.ts +13 -0
- package/dist/diff/WorkflowDiffer.js +429 -0
- package/dist/diff/formatDiff.d.ts +10 -0
- package/dist/diff/formatDiff.js +220 -0
- package/dist/diff/impact.d.ts +29 -0
- package/dist/diff/impact.js +119 -0
- package/dist/diff/index.d.ts +10 -0
- package/dist/diff/index.js +9 -0
- package/dist/diff/types.d.ts +138 -0
- package/dist/diff/types.js +35 -0
- package/dist/doc-metadata/extractors/annotations.d.ts +56 -0
- package/dist/doc-metadata/extractors/annotations.js +337 -0
- package/dist/doc-metadata/extractors/cli-commands.d.ts +17 -0
- package/dist/doc-metadata/extractors/cli-commands.js +355 -0
- package/dist/doc-metadata/extractors/mcp-tools.d.ts +16 -0
- package/dist/doc-metadata/extractors/mcp-tools.js +689 -0
- package/dist/doc-metadata/extractors/plugin-api.d.ts +19 -0
- package/dist/doc-metadata/extractors/plugin-api.js +279 -0
- package/dist/doc-metadata/index.d.ts +5 -0
- package/dist/doc-metadata/index.js +4 -0
- package/dist/doc-metadata/types.d.ts +120 -0
- package/dist/doc-metadata/types.js +5 -0
- package/dist/editor-completions/annotationValues.d.ts +12 -0
- package/dist/editor-completions/annotationValues.js +138 -0
- package/dist/editor-completions/contextParser.d.ts +40 -0
- package/dist/editor-completions/contextParser.js +410 -0
- package/dist/editor-completions/dataTypes.d.ts +16 -0
- package/dist/editor-completions/dataTypes.js +95 -0
- package/dist/editor-completions/goToDefinition.d.ts +27 -0
- package/dist/editor-completions/goToDefinition.js +112 -0
- package/dist/editor-completions/index.d.ts +39 -0
- package/dist/editor-completions/index.js +181 -0
- package/dist/editor-completions/jsDocAnnotations.d.ts +29 -0
- package/dist/editor-completions/jsDocAnnotations.js +357 -0
- package/dist/editor-completions/modifierCompletions.d.ts +17 -0
- package/dist/editor-completions/modifierCompletions.js +197 -0
- package/dist/editor-completions/types.d.ts +119 -0
- package/dist/editor-completions/types.js +8 -0
- package/dist/export/index.d.ts +68 -0
- package/dist/export/index.js +1074 -0
- package/dist/export/templates.d.ts +24 -0
- package/dist/export/templates.js +186 -0
- package/dist/friendly-errors.d.ts +35 -0
- package/dist/friendly-errors.js +375 -0
- package/dist/function-like.d.ts +38 -0
- package/dist/function-like.js +83 -0
- package/dist/generated-branding.d.ts +16 -0
- package/dist/generated-branding.js +22 -0
- package/dist/generator/async-detection.d.ts +27 -0
- package/dist/generator/async-detection.js +56 -0
- package/dist/generator/code-utils.d.ts +76 -0
- package/dist/generator/code-utils.js +410 -0
- package/dist/generator/control-flow.d.ts +54 -0
- package/dist/generator/control-flow.js +284 -0
- package/dist/generator/inngest.d.ts +53 -0
- package/dist/generator/inngest.js +1126 -0
- package/dist/generator/scope-function-generator.d.ts +78 -0
- package/dist/generator/scope-function-generator.js +360 -0
- package/dist/generator/unified.d.ts +42 -0
- package/dist/generator/unified.js +1504 -0
- package/dist/generator.d.ts +54 -0
- package/dist/generator.js +100 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +89 -0
- package/dist/jsdoc-parser.d.ts +308 -0
- package/dist/jsdoc-parser.js +923 -0
- package/dist/jsdoc-port-sync/constants.d.ts +41 -0
- package/dist/jsdoc-port-sync/constants.js +103 -0
- package/dist/jsdoc-port-sync/diff.d.ts +76 -0
- package/dist/jsdoc-port-sync/diff.js +319 -0
- package/dist/jsdoc-port-sync/index.d.ts +42 -0
- package/dist/jsdoc-port-sync/index.js +45 -0
- package/dist/jsdoc-port-sync/port-parser.d.ts +68 -0
- package/dist/jsdoc-port-sync/port-parser.js +579 -0
- package/dist/jsdoc-port-sync/rename.d.ts +21 -0
- package/dist/jsdoc-port-sync/rename.js +256 -0
- package/dist/jsdoc-port-sync/signature-parser.d.ts +104 -0
- package/dist/jsdoc-port-sync/signature-parser.js +559 -0
- package/dist/jsdoc-port-sync/sync.d.ts +36 -0
- package/dist/jsdoc-port-sync/sync.js +644 -0
- package/dist/jsdoc-port-sync.d.ts +10 -0
- package/dist/jsdoc-port-sync.js +10 -0
- package/dist/marketplace/index.d.ts +11 -0
- package/dist/marketplace/index.js +10 -0
- package/dist/marketplace/manifest.d.ts +32 -0
- package/dist/marketplace/manifest.js +176 -0
- package/dist/marketplace/registry.d.ts +30 -0
- package/dist/marketplace/registry.js +100 -0
- package/dist/marketplace/types.d.ts +154 -0
- package/dist/marketplace/types.js +9 -0
- package/dist/marketplace/validator.d.ts +13 -0
- package/dist/marketplace/validator.js +131 -0
- package/dist/mcp/auto-registration.d.ts +3 -0
- package/dist/mcp/auto-registration.js +62 -0
- package/dist/mcp/editor-connection.d.ts +50 -0
- package/dist/mcp/editor-connection.js +125 -0
- package/dist/mcp/event-buffer.d.ts +62 -0
- package/dist/mcp/event-buffer.js +150 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/resources.d.ts +14 -0
- package/dist/mcp/resources.js +55 -0
- package/dist/mcp/response-utils.d.ts +63 -0
- package/dist/mcp/response-utils.js +89 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +99 -0
- package/dist/mcp/tools-diagram.d.ts +8 -0
- package/dist/mcp/tools-diagram.js +53 -0
- package/dist/mcp/tools-editor.d.ts +5 -0
- package/dist/mcp/tools-editor.js +190 -0
- package/dist/mcp/tools-export.d.ts +9 -0
- package/dist/mcp/tools-export.js +180 -0
- package/dist/mcp/tools-marketplace.d.ts +9 -0
- package/dist/mcp/tools-marketplace.js +132 -0
- package/dist/mcp/tools-pattern.d.ts +3 -0
- package/dist/mcp/tools-pattern.js +783 -0
- package/dist/mcp/tools-query.d.ts +3 -0
- package/dist/mcp/tools-query.js +364 -0
- package/dist/mcp/tools-template.d.ts +10 -0
- package/dist/mcp/tools-template.js +119 -0
- package/dist/mcp/types.d.ts +70 -0
- package/dist/mcp/types.js +8 -0
- package/dist/mcp/workflow-executor.d.ts +47 -0
- package/dist/mcp/workflow-executor.js +133 -0
- package/dist/migration/registry.d.ts +30 -0
- package/dist/migration/registry.js +29 -0
- package/dist/node-types-generator.d.ts +49 -0
- package/dist/node-types-generator.js +139 -0
- package/dist/npm-packages.d.ts +56 -0
- package/dist/npm-packages.js +255 -0
- package/dist/parser.d.ts +204 -0
- package/dist/parser.js +2100 -0
- package/dist/plugin/PluginPanel.d.ts +12 -0
- package/dist/plugin/PluginPanel.js +5 -0
- package/dist/plugin/index.d.ts +13 -0
- package/dist/plugin/index.js +14 -0
- package/dist/plugin/types.d.ts +75 -0
- package/dist/plugin/types.js +8 -0
- package/dist/resolve-package-types.d.ts +17 -0
- package/dist/resolve-package-types.js +123 -0
- package/dist/runtime/CancellationError.d.ts +11 -0
- package/dist/runtime/CancellationError.js +20 -0
- package/dist/runtime/ExecutionContext.d.ts +146 -0
- package/dist/runtime/ExecutionContext.js +235 -0
- package/dist/runtime/builtin-functions.d.ts +8 -0
- package/dist/runtime/builtin-functions.js +549 -0
- package/dist/runtime/events.d.ts +50 -0
- package/dist/runtime/events.js +2 -0
- package/dist/runtime/function-registry.d.ts +59 -0
- package/dist/runtime/function-registry.js +66 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/parameter-resolver.d.ts +62 -0
- package/dist/runtime/parameter-resolver.js +113 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +6 -0
- package/dist/server/types.d.ts +93 -0
- package/dist/server/types.js +5 -0
- package/dist/server/webhook-server.d.ts +50 -0
- package/dist/server/webhook-server.js +269 -0
- package/dist/server/workflow-registry.d.ts +61 -0
- package/dist/server/workflow-registry.js +202 -0
- package/dist/shared-project.d.ts +9 -0
- package/dist/shared-project.js +28 -0
- package/dist/sugar-optimizer.d.ts +40 -0
- package/dist/sugar-optimizer.js +387 -0
- package/dist/testing/assertions.d.ts +51 -0
- package/dist/testing/assertions.js +127 -0
- package/dist/testing/index.d.ts +30 -0
- package/dist/testing/index.js +24 -0
- package/dist/testing/mock-approval.d.ts +81 -0
- package/dist/testing/mock-approval.js +98 -0
- package/dist/testing/mock-llm.d.ts +124 -0
- package/dist/testing/mock-llm.js +119 -0
- package/dist/testing/recorder.d.ts +72 -0
- package/dist/testing/recorder.js +70 -0
- package/dist/testing/replayer.d.ts +56 -0
- package/dist/testing/replayer.js +143 -0
- package/dist/testing/token-tracker.d.ts +71 -0
- package/dist/testing/token-tracker.js +94 -0
- package/dist/type-checker.d.ts +42 -0
- package/dist/type-checker.js +190 -0
- package/dist/type-mappings.d.ts +29 -0
- package/dist/type-mappings.js +125 -0
- package/dist/types/branded-ports.d.ts +151 -0
- package/dist/types/branded-ports.js +121 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +5 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +15 -0
- package/dist/utils/error-utils.d.ts +15 -0
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/lru-cache.d.ts +15 -0
- package/dist/utils/lru-cache.js +40 -0
- package/dist/utils/port-ordering.d.ts +26 -0
- package/dist/utils/port-ordering.js +88 -0
- package/dist/utils/port-tag-utils.d.ts +23 -0
- package/dist/utils/port-tag-utils.js +41 -0
- package/dist/utils/string-distance.d.ts +14 -0
- package/dist/utils/string-distance.js +56 -0
- package/dist/validation/agent-detection.d.ts +33 -0
- package/dist/validation/agent-detection.js +115 -0
- package/dist/validation/agent-rules.d.ts +48 -0
- package/dist/validation/agent-rules.js +262 -0
- package/dist/validator.d.ts +92 -0
- package/dist/validator.js +970 -0
- package/package.json +109 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,2100 @@
|
|
|
1
|
+
import { extractFunctionLikes } from './function-like.js';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { jsdocParser } from './jsdoc-parser.js';
|
|
6
|
+
import { EXECUTION_STRATEGIES, RESERVED_PORT_NAMES, isControlFlowPort } from './constants.js';
|
|
7
|
+
import { getErrorMessage } from './utils/error-utils.js';
|
|
8
|
+
import { assignImplicitPortOrders } from './utils/port-ordering.js';
|
|
9
|
+
import { stripGeneratedSections, hasInPlaceMarkers } from './api/generate-in-place.js';
|
|
10
|
+
import { inferDataTypeFromTS } from './type-mappings.js';
|
|
11
|
+
import { generateJSDocPortTag } from './annotation-generator.js';
|
|
12
|
+
import { resolvePackageTypesPath } from './resolve-package-types.js';
|
|
13
|
+
import { getPackageExports } from './npm-packages.js';
|
|
14
|
+
import { getSharedProject } from './shared-project.js';
|
|
15
|
+
import { LRUCache } from './utils/lru-cache.js';
|
|
16
|
+
/**
|
|
17
|
+
* Convert a TExternalNodeType to a TNodeTypeAST with sensible defaults.
|
|
18
|
+
* Used to merge runtime-loaded node types into the parser's available types.
|
|
19
|
+
*/
|
|
20
|
+
function externalToAST(ext) {
|
|
21
|
+
const inputs = {};
|
|
22
|
+
const outputs = {};
|
|
23
|
+
if (ext.ports) {
|
|
24
|
+
for (const port of ext.ports) {
|
|
25
|
+
const def = {
|
|
26
|
+
dataType: port.type || 'ANY',
|
|
27
|
+
...(port.defaultLabel && { label: port.defaultLabel }),
|
|
28
|
+
};
|
|
29
|
+
if (port.direction === 'OUTPUT') {
|
|
30
|
+
outputs[port.name] = def;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
inputs[port.name] = def;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Ensure mandatory ports exist
|
|
38
|
+
if (!inputs.execute) {
|
|
39
|
+
inputs.execute = { dataType: 'STEP', label: 'Execute' };
|
|
40
|
+
}
|
|
41
|
+
if (!outputs.onSuccess) {
|
|
42
|
+
outputs.onSuccess = { dataType: 'STEP', label: 'On Success', isControlFlow: true };
|
|
43
|
+
}
|
|
44
|
+
if (!outputs.onFailure) {
|
|
45
|
+
outputs.onFailure = {
|
|
46
|
+
dataType: 'STEP',
|
|
47
|
+
label: 'On Failure',
|
|
48
|
+
isControlFlow: true,
|
|
49
|
+
failure: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
type: 'NodeType',
|
|
54
|
+
name: ext.name,
|
|
55
|
+
functionName: ext.functionName || ext.name,
|
|
56
|
+
inputs,
|
|
57
|
+
outputs,
|
|
58
|
+
hasSuccessPort: 'onSuccess' in outputs,
|
|
59
|
+
hasFailurePort: 'onFailure' in outputs,
|
|
60
|
+
isAsync: false,
|
|
61
|
+
executeWhen: EXECUTION_STRATEGIES.CONJUNCTION,
|
|
62
|
+
variant: 'FUNCTION',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Port ordering functions imported from ./utils/port-ordering
|
|
66
|
+
/** Exposed for tests that need direct access to the shared ts-morph Project */
|
|
67
|
+
export function getParserProject() {
|
|
68
|
+
return getSharedProject();
|
|
69
|
+
}
|
|
70
|
+
export class AnnotationParser {
|
|
71
|
+
project;
|
|
72
|
+
importCache = new LRUCache(200);
|
|
73
|
+
importStack = new Set();
|
|
74
|
+
parseCache = new LRUCache(100);
|
|
75
|
+
constructor() {
|
|
76
|
+
this.project = getSharedProject();
|
|
77
|
+
}
|
|
78
|
+
computeHash(content) {
|
|
79
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
80
|
+
}
|
|
81
|
+
detectMinorEdit(original, updated) {
|
|
82
|
+
let start = 0;
|
|
83
|
+
const minLen = Math.min(original.length, updated.length);
|
|
84
|
+
while (start < minLen && original[start] === updated[start])
|
|
85
|
+
start++;
|
|
86
|
+
let endOrig = original.length;
|
|
87
|
+
let endNew = updated.length;
|
|
88
|
+
while (endOrig > start && endNew > start && original[endOrig - 1] === updated[endNew - 1]) {
|
|
89
|
+
endOrig--;
|
|
90
|
+
endNew--;
|
|
91
|
+
}
|
|
92
|
+
const changedRegion = updated.slice(start, endNew);
|
|
93
|
+
// Structural patterns require full re-parse
|
|
94
|
+
const structural = /import\b|export\b|@flowWeaver|@input\b|@output\b|function\s+\w+\s*\(|const\s+\w+\s*=|let\s+\w+\s*=|var\s+\w+\s*=|@node\b|@connect\b/;
|
|
95
|
+
if (structural.test(changedRegion)) {
|
|
96
|
+
return { isMinor: false, affectedFunctions: [] };
|
|
97
|
+
}
|
|
98
|
+
// For now, return isMinor: true but no affected functions (conservative approach)
|
|
99
|
+
// This means we'll still do a full parse but the infrastructure is in place
|
|
100
|
+
return { isMinor: true, affectedFunctions: [] };
|
|
101
|
+
}
|
|
102
|
+
patchAST(filePath, cached, newContent, _affectedFunctions) {
|
|
103
|
+
try {
|
|
104
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
105
|
+
if (!sourceFile)
|
|
106
|
+
return null;
|
|
107
|
+
sourceFile.replaceWithText(newContent);
|
|
108
|
+
// Re-extract all node types (conservative approach for now)
|
|
109
|
+
const warnings = [];
|
|
110
|
+
const nodeTypes = this.extractNodeTypes(sourceFile, warnings);
|
|
111
|
+
const result = {
|
|
112
|
+
...cached.result,
|
|
113
|
+
nodeTypes,
|
|
114
|
+
warnings: [...cached.result.warnings, ...warnings],
|
|
115
|
+
};
|
|
116
|
+
this.parseCache.set(filePath, {
|
|
117
|
+
mtime: fs.statSync(filePath).mtimeMs,
|
|
118
|
+
contentHash: this.computeHash(newContent),
|
|
119
|
+
result,
|
|
120
|
+
});
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
parse(filePath, externalNodeTypes) {
|
|
128
|
+
const stats = fs.statSync(filePath);
|
|
129
|
+
const hasExternalTypes = externalNodeTypes && externalNodeTypes.length > 0;
|
|
130
|
+
// Skip cache when external node types are provided — cache was built without them
|
|
131
|
+
if (!hasExternalTypes) {
|
|
132
|
+
const cached = this.parseCache.get(filePath);
|
|
133
|
+
// FAST PATH 1: mtime unchanged
|
|
134
|
+
if (cached && cached.mtime === stats.mtimeMs) {
|
|
135
|
+
return cached.result;
|
|
136
|
+
}
|
|
137
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
138
|
+
const content = hasInPlaceMarkers(rawContent)
|
|
139
|
+
? stripGeneratedSections(rawContent)
|
|
140
|
+
: rawContent;
|
|
141
|
+
const hash = this.computeHash(content);
|
|
142
|
+
// FAST PATH 2: content hash unchanged (save without edit)
|
|
143
|
+
if (cached && cached.contentHash === hash) {
|
|
144
|
+
cached.mtime = stats.mtimeMs;
|
|
145
|
+
return cached.result;
|
|
146
|
+
}
|
|
147
|
+
// FAST PATH 3: Incremental patching disabled — re-enable when detectMinorEdit
|
|
148
|
+
// returns affected functions. Infrastructure preserved in detectMinorEdit/patchAST.
|
|
149
|
+
// FALLBACK: Full parse
|
|
150
|
+
return this.fullParse(filePath, content, hash, stats.mtimeMs);
|
|
151
|
+
}
|
|
152
|
+
// External types provided — always do a full parse without caching the result
|
|
153
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
154
|
+
const content = hasInPlaceMarkers(rawContent) ? stripGeneratedSections(rawContent) : rawContent;
|
|
155
|
+
const hash = this.computeHash(content);
|
|
156
|
+
return this.fullParse(filePath, content, hash, stats.mtimeMs, externalNodeTypes);
|
|
157
|
+
}
|
|
158
|
+
fullParse(filePath, content, hash, mtimeMs, externalNodeTypes) {
|
|
159
|
+
// Reset import tracking for new parse
|
|
160
|
+
this.importStack.clear();
|
|
161
|
+
const errors = [];
|
|
162
|
+
const warnings = [];
|
|
163
|
+
const sourceFile = this.project.createSourceFile(filePath, content, { overwrite: true });
|
|
164
|
+
// Add current file to import stack BEFORE processing imports
|
|
165
|
+
this.importStack.add(filePath);
|
|
166
|
+
const localNodeTypes = this.extractNodeTypes(sourceFile, warnings);
|
|
167
|
+
const importedNodeTypes = this.extractImportedNodeTypes(sourceFile, filePath);
|
|
168
|
+
// First pass: extract workflow signatures to enable same-file workflow invocation
|
|
169
|
+
const workflowSignatures = this.extractWorkflowSignatures(sourceFile, filePath, warnings);
|
|
170
|
+
const sameFileWorkflowNodeTypes = workflowSignatures.map((wf) => this.workflowToNodeType(wf));
|
|
171
|
+
const nodeTypes = [...localNodeTypes, ...importedNodeTypes, ...sameFileWorkflowNodeTypes];
|
|
172
|
+
// Merge external (runtime-loaded) node types so the parser can validate references
|
|
173
|
+
if (externalNodeTypes?.length) {
|
|
174
|
+
for (const ext of externalNodeTypes) {
|
|
175
|
+
const alreadyKnown = nodeTypes.some((nt) => nt.name === ext.name || nt.functionName === ext.name);
|
|
176
|
+
if (!alreadyKnown) {
|
|
177
|
+
nodeTypes.push(externalToAST(ext));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Auto-infer node types from unannotated functions referenced by @node
|
|
182
|
+
const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
|
|
183
|
+
nodeTypes.push(...inferredNodeTypes);
|
|
184
|
+
const workflows = this.extractWorkflows(sourceFile, nodeTypes, filePath, errors, warnings);
|
|
185
|
+
const patterns = this.extractPatterns(sourceFile, nodeTypes, filePath, errors, warnings);
|
|
186
|
+
const result = { workflows, nodeTypes, patterns, errors, warnings };
|
|
187
|
+
// Clean up source file to prevent ts-morph Project bloat
|
|
188
|
+
// (results are captured in the returned AST, source file is no longer needed)
|
|
189
|
+
this.project.removeSourceFile(sourceFile);
|
|
190
|
+
// Only cache when no external types were used (cache should reflect file-only state)
|
|
191
|
+
if (!externalNodeTypes?.length) {
|
|
192
|
+
this.parseCache.set(filePath, {
|
|
193
|
+
mtime: mtimeMs,
|
|
194
|
+
contentHash: hash,
|
|
195
|
+
result,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Parse workflow from a string instead of a file path.
|
|
202
|
+
* Useful for testing and in-memory operations.
|
|
203
|
+
*
|
|
204
|
+
* Note: Imports from other workflow files are NOT supported in this mode
|
|
205
|
+
* since there's no filesystem context. Use parse() for files with imports.
|
|
206
|
+
*
|
|
207
|
+
* @param code - TypeScript source code containing workflow definitions
|
|
208
|
+
* @param virtualPath - Virtual file path for error messages (default: 'virtual.ts')
|
|
209
|
+
* @returns ParseResult with workflows and nodeTypes
|
|
210
|
+
*/
|
|
211
|
+
parseFromString(code, virtualPath = 'virtual.ts') {
|
|
212
|
+
// Reset import tracking
|
|
213
|
+
this.importStack.clear();
|
|
214
|
+
// Remove existing virtual file if present
|
|
215
|
+
const existingFile = this.project.getSourceFile(virtualPath);
|
|
216
|
+
if (existingFile) {
|
|
217
|
+
this.project.removeSourceFile(existingFile);
|
|
218
|
+
}
|
|
219
|
+
// Create source file from string
|
|
220
|
+
const sourceFile = this.project.createSourceFile(virtualPath, code, { overwrite: true });
|
|
221
|
+
const errors = [];
|
|
222
|
+
const warnings = [];
|
|
223
|
+
const localNodeTypes = this.extractNodeTypes(sourceFile, warnings);
|
|
224
|
+
// First pass: extract workflow signatures to enable same-file workflow invocation
|
|
225
|
+
const workflowSignatures = this.extractWorkflowSignatures(sourceFile, virtualPath, warnings);
|
|
226
|
+
const sameFileWorkflowNodeTypes = workflowSignatures.map((wf) => this.workflowToNodeType(wf));
|
|
227
|
+
const nodeTypes = [...localNodeTypes, ...sameFileWorkflowNodeTypes];
|
|
228
|
+
// Auto-infer node types from unannotated functions referenced by @node
|
|
229
|
+
const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
|
|
230
|
+
nodeTypes.push(...inferredNodeTypes);
|
|
231
|
+
// Note: imports not supported for virtual files - would need filesystem access
|
|
232
|
+
const workflows = this.extractWorkflows(sourceFile, nodeTypes, virtualPath, errors, warnings);
|
|
233
|
+
const patterns = this.extractPatterns(sourceFile, nodeTypes, virtualPath, errors, warnings);
|
|
234
|
+
// Clean up virtual source file to prevent memory bloat
|
|
235
|
+
// (tests create many unique virtual paths that accumulate)
|
|
236
|
+
this.project.removeSourceFile(sourceFile);
|
|
237
|
+
return {
|
|
238
|
+
workflows,
|
|
239
|
+
nodeTypes,
|
|
240
|
+
patterns,
|
|
241
|
+
errors,
|
|
242
|
+
warnings,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
clearCache() {
|
|
246
|
+
this.importCache.clear();
|
|
247
|
+
this.parseCache.clear();
|
|
248
|
+
}
|
|
249
|
+
/** Clear only the parse result cache, keeping the import/node-type cache intact. */
|
|
250
|
+
clearParseCache() {
|
|
251
|
+
this.parseCache.clear();
|
|
252
|
+
}
|
|
253
|
+
resolveModulePath(moduleSpecifier, currentDir) {
|
|
254
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
255
|
+
// If already has extension, check if exists
|
|
256
|
+
const hasExtension = extensions.some((ext) => moduleSpecifier.endsWith(ext));
|
|
257
|
+
if (hasExtension) {
|
|
258
|
+
const fullPath = path.resolve(currentDir, moduleSpecifier);
|
|
259
|
+
return fs.existsSync(fullPath) ? fullPath : null;
|
|
260
|
+
}
|
|
261
|
+
// Try each extension in order
|
|
262
|
+
for (const ext of extensions) {
|
|
263
|
+
const fullPath = path.resolve(currentDir, moduleSpecifier + ext);
|
|
264
|
+
if (fs.existsSync(fullPath)) {
|
|
265
|
+
return fullPath;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Try as directory with package.json main field or index file
|
|
269
|
+
const dirPath = path.resolve(currentDir, moduleSpecifier);
|
|
270
|
+
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
271
|
+
// Check package.json main field
|
|
272
|
+
const pkgPath = path.join(dirPath, 'package.json');
|
|
273
|
+
if (fs.existsSync(pkgPath)) {
|
|
274
|
+
try {
|
|
275
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
276
|
+
if (pkg.main) {
|
|
277
|
+
const mainPath = path.resolve(dirPath, pkg.main);
|
|
278
|
+
if (fs.existsSync(mainPath)) {
|
|
279
|
+
return mainPath;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
console.warn(`Failed to parse package.json at ${pkgPath}: ${getErrorMessage(e)}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Try index files
|
|
288
|
+
for (const ext of extensions) {
|
|
289
|
+
const indexPath = path.join(dirPath, `index${ext}`);
|
|
290
|
+
if (fs.existsSync(indexPath)) {
|
|
291
|
+
return indexPath;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
extractImportedNodeTypes(sourceFile, currentFilePath) {
|
|
298
|
+
const importedNodeTypes = [];
|
|
299
|
+
const imports = sourceFile.getImportDeclarations();
|
|
300
|
+
for (const importDecl of imports) {
|
|
301
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
302
|
+
// Skip if module specifier is undefined or not a relative import
|
|
303
|
+
// Any .ts file with @flowWeaver annotations can be imported.
|
|
304
|
+
if (!moduleSpecifier) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (!moduleSpecifier.startsWith('.')) {
|
|
308
|
+
const packageNodeTypes = this.resolveNpmPackageTypes(importDecl, moduleSpecifier, currentFilePath);
|
|
309
|
+
importedNodeTypes.push(...packageNodeTypes);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const currentDir = path.dirname(currentFilePath);
|
|
313
|
+
const importedFilePath = this.resolveModulePath(moduleSpecifier, currentDir);
|
|
314
|
+
// Validate import path exists
|
|
315
|
+
if (!importedFilePath) {
|
|
316
|
+
throw new Error(`Import error: File not found for "${moduleSpecifier}"\n` +
|
|
317
|
+
` Imported from: ${currentFilePath}\n` +
|
|
318
|
+
` Searched extensions: .ts, .tsx, .js, .jsx`);
|
|
319
|
+
}
|
|
320
|
+
// Check for circular dependencies
|
|
321
|
+
if (this.importStack.has(importedFilePath)) {
|
|
322
|
+
const cycle = Array.from(this.importStack).concat(importedFilePath);
|
|
323
|
+
throw new Error(`Circular dependency detected:\n ${cycle.join('\n -> ')}`);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
// Check cache first
|
|
327
|
+
let nodeTypes;
|
|
328
|
+
if (this.importCache.has(importedFilePath)) {
|
|
329
|
+
nodeTypes = this.importCache.get(importedFilePath);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Add to import stack for circular dependency detection
|
|
333
|
+
this.importStack.add(importedFilePath);
|
|
334
|
+
try {
|
|
335
|
+
const importedRaw = fs.readFileSync(importedFilePath, 'utf-8');
|
|
336
|
+
const importedContent = hasInPlaceMarkers(importedRaw)
|
|
337
|
+
? stripGeneratedSections(importedRaw)
|
|
338
|
+
: importedRaw;
|
|
339
|
+
const importedFile = this.project.createSourceFile(importedFilePath, importedContent, {
|
|
340
|
+
overwrite: true,
|
|
341
|
+
});
|
|
342
|
+
const importWarnings = [];
|
|
343
|
+
const localNodeTypes = this.extractNodeTypes(importedFile, importWarnings);
|
|
344
|
+
// Recursively process imports (enables circular dependency detection)
|
|
345
|
+
const importedFromFile = this.extractImportedNodeTypes(importedFile, importedFilePath);
|
|
346
|
+
// Also extract workflows and convert them to node types
|
|
347
|
+
const workflows = this.extractWorkflows(importedFile, [...localNodeTypes, ...importedFromFile], importedFilePath, [], importWarnings);
|
|
348
|
+
const workflowAsNodeTypes = workflows.map((wf) => this.workflowToNodeType(wf));
|
|
349
|
+
nodeTypes = [...localNodeTypes, ...importedFromFile, ...workflowAsNodeTypes];
|
|
350
|
+
// Pre-infer all unannotated functions so the named-import filter can resolve them
|
|
351
|
+
const inferredFromImport = this.inferAllUnannotatedFunctions(importedFile, nodeTypes);
|
|
352
|
+
nodeTypes.push(...inferredFromImport);
|
|
353
|
+
// Clean up imported source file to prevent Project bloat
|
|
354
|
+
this.project.removeSourceFile(importedFile);
|
|
355
|
+
// Cache the parsed node types
|
|
356
|
+
this.importCache.set(importedFilePath, nodeTypes);
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
// Remove from stack after processing
|
|
360
|
+
this.importStack.delete(importedFilePath);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Extract only the named imports
|
|
364
|
+
const importedNames = new Set();
|
|
365
|
+
importDecl.getNamedImports().forEach((namedImport) => {
|
|
366
|
+
importedNames.add(namedImport.getName());
|
|
367
|
+
});
|
|
368
|
+
// Only include imports that are actually node types
|
|
369
|
+
// (other imports may be regular TypeScript exports like types, constants, etc.)
|
|
370
|
+
nodeTypes.forEach((nodeType) => {
|
|
371
|
+
if (importedNames.has(nodeType.functionName)) {
|
|
372
|
+
importedNodeTypes.push({
|
|
373
|
+
...nodeType,
|
|
374
|
+
sourceLocation: {
|
|
375
|
+
file: importedFilePath,
|
|
376
|
+
line: nodeType.sourceLocation?.line || 0,
|
|
377
|
+
column: nodeType.sourceLocation?.column || 0,
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
// Re-throw with better context
|
|
385
|
+
if (error instanceof Error) {
|
|
386
|
+
throw new Error(`Failed to process import from ${importedFilePath}:\n ${error.message}`);
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return importedNodeTypes;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Resolve npm package imports to node types by reading `.d.ts` declarations.
|
|
395
|
+
* Only named imports of exported functions are resolved.
|
|
396
|
+
*/
|
|
397
|
+
resolveNpmPackageTypes(importDecl, moduleSpecifier, currentFilePath) {
|
|
398
|
+
// Only handle named imports
|
|
399
|
+
const namedImports = importDecl.getNamedImports();
|
|
400
|
+
if (namedImports.length === 0)
|
|
401
|
+
return [];
|
|
402
|
+
const importedNames = new Set();
|
|
403
|
+
namedImports.forEach((ni) => importedNames.add(ni.getName()));
|
|
404
|
+
// Check cache
|
|
405
|
+
const cacheKey = `npm:${moduleSpecifier}`;
|
|
406
|
+
if (this.importCache.has(cacheKey)) {
|
|
407
|
+
return this.importCache.get(cacheKey).filter((nt) => importedNames.has(nt.functionName));
|
|
408
|
+
}
|
|
409
|
+
// Resolve .d.ts path
|
|
410
|
+
const currentDir = path.dirname(currentFilePath);
|
|
411
|
+
const dtsPath = resolvePackageTypesPath(moduleSpecifier, currentDir);
|
|
412
|
+
if (!dtsPath)
|
|
413
|
+
return [];
|
|
414
|
+
try {
|
|
415
|
+
const dtsContent = fs.readFileSync(dtsPath, 'utf-8');
|
|
416
|
+
const dtsFile = this.project.createSourceFile(`__npm_dts__/${moduleSpecifier}.d.ts`, dtsContent, { overwrite: true });
|
|
417
|
+
const fns = extractFunctionLikes(dtsFile);
|
|
418
|
+
const allNodeTypes = [];
|
|
419
|
+
const seenNames = new Set();
|
|
420
|
+
for (const fn of fns) {
|
|
421
|
+
const fnName = fn.getName();
|
|
422
|
+
if (!fnName)
|
|
423
|
+
continue;
|
|
424
|
+
// Skip duplicate function names (overloaded declarations in .d.ts)
|
|
425
|
+
if (seenNames.has(fnName))
|
|
426
|
+
continue;
|
|
427
|
+
seenNames.add(fnName);
|
|
428
|
+
const nodeType = this.inferNodeTypeFromFunction(fn, fnName, dtsPath);
|
|
429
|
+
// Mark as npm package import and prevent inlining
|
|
430
|
+
nodeType.importSource = moduleSpecifier;
|
|
431
|
+
nodeType.functionText = undefined;
|
|
432
|
+
allNodeTypes.push(nodeType);
|
|
433
|
+
}
|
|
434
|
+
// Clean up the temporary source file
|
|
435
|
+
this.project.removeSourceFile(dtsFile);
|
|
436
|
+
// Cache all node types from this package
|
|
437
|
+
this.importCache.set(cacheKey, allNodeTypes);
|
|
438
|
+
// Return only the ones in the import statement
|
|
439
|
+
return allNodeTypes.filter((nt) => importedNames.has(nt.functionName));
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Silently skip packages whose .d.ts can't be parsed
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Resolve an @fwImport annotation to a properly inferred node type.
|
|
448
|
+
* Supports both npm packages (e.g., "lodash") and relative paths (e.g., "./utils").
|
|
449
|
+
*
|
|
450
|
+
* @param imp - The import annotation from JSDoc
|
|
451
|
+
* @param currentFilePath - Path of the workflow file containing the @fwImport
|
|
452
|
+
* @param warnings - Array to collect warnings
|
|
453
|
+
* @returns Inferred TNodeTypeAST, or a stub if inference fails
|
|
454
|
+
*/
|
|
455
|
+
resolveImportAnnotation(imp, currentFilePath, warnings) {
|
|
456
|
+
const currentDir = path.dirname(currentFilePath);
|
|
457
|
+
// Determine if this is a relative path import or an npm package
|
|
458
|
+
if (imp.importSource.startsWith('.')) {
|
|
459
|
+
// Relative path import - resolve local file and infer
|
|
460
|
+
return this.resolveLocalImportAnnotation(imp, currentDir, warnings);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
// npm package import - use .d.ts inference
|
|
464
|
+
return this.resolveNpmImportAnnotation(imp, currentDir);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Resolve a relative path @fwImport to a node type by reading the local file.
|
|
469
|
+
* Includes circular dependency detection using importStack.
|
|
470
|
+
*/
|
|
471
|
+
resolveLocalImportAnnotation(imp, currentDir, warnings) {
|
|
472
|
+
const importedFilePath = this.resolveModulePath(imp.importSource, currentDir);
|
|
473
|
+
if (!importedFilePath) {
|
|
474
|
+
// Gap 3: Warn when relative path doesn't resolve
|
|
475
|
+
warnings.push(`@fwImport: Could not resolve "${imp.importSource}" from ${currentDir}`);
|
|
476
|
+
return this.createImportStub(imp);
|
|
477
|
+
}
|
|
478
|
+
// Gap 1: Circular dependency detection
|
|
479
|
+
if (this.importStack.has(importedFilePath)) {
|
|
480
|
+
const cycle = Array.from(this.importStack).concat(importedFilePath);
|
|
481
|
+
warnings.push(`@fwImport: Circular dependency detected:\n ${cycle.join('\n -> ')}`);
|
|
482
|
+
return this.createImportStub(imp);
|
|
483
|
+
}
|
|
484
|
+
// Add to import stack before processing
|
|
485
|
+
this.importStack.add(importedFilePath);
|
|
486
|
+
try {
|
|
487
|
+
const importedContent = fs.readFileSync(importedFilePath, 'utf-8');
|
|
488
|
+
const importedFile = this.project.createSourceFile(importedFilePath, importedContent, {
|
|
489
|
+
overwrite: true,
|
|
490
|
+
});
|
|
491
|
+
const fns = extractFunctionLikes(importedFile);
|
|
492
|
+
const fn = fns.find((f) => f.getName() === imp.functionName);
|
|
493
|
+
if (!fn) {
|
|
494
|
+
// Function not found in file - return stub
|
|
495
|
+
this.project.removeSourceFile(importedFile);
|
|
496
|
+
return this.createImportStub(imp);
|
|
497
|
+
}
|
|
498
|
+
// Infer BEFORE removing the source file (ts-morph needs it)
|
|
499
|
+
const nodeType = this.inferNodeTypeFromFunction(fn, imp.name, importedFilePath);
|
|
500
|
+
nodeType.importSource = imp.importSource;
|
|
501
|
+
nodeType.functionText = undefined; // Don't inline external code
|
|
502
|
+
// Clean up after inference is complete
|
|
503
|
+
this.project.removeSourceFile(importedFile);
|
|
504
|
+
return nodeType;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Graceful fallback on any error
|
|
508
|
+
return this.createImportStub(imp);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
// Always remove from import stack
|
|
512
|
+
this.importStack.delete(importedFilePath);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Resolve an npm package @fwImport to a node type by reading .d.ts declarations.
|
|
517
|
+
*/
|
|
518
|
+
resolveNpmImportAnnotation(imp, currentDir) {
|
|
519
|
+
// Check cache
|
|
520
|
+
const cacheKey = `npm:${imp.importSource}`;
|
|
521
|
+
if (this.importCache.has(cacheKey)) {
|
|
522
|
+
const cached = this.importCache.get(cacheKey);
|
|
523
|
+
const found = cached.find((nt) => nt.functionName === imp.functionName);
|
|
524
|
+
if (found) {
|
|
525
|
+
// Return a copy with the correct name from @fwImport
|
|
526
|
+
return { ...found, name: imp.name, importSource: imp.importSource };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Resolve .d.ts path
|
|
530
|
+
const dtsPath = resolvePackageTypesPath(imp.importSource, currentDir);
|
|
531
|
+
if (!dtsPath) {
|
|
532
|
+
return this.createImportStub(imp);
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const dtsContent = fs.readFileSync(dtsPath, 'utf-8');
|
|
536
|
+
const dtsFile = this.project.createSourceFile(`__npm_dts__/${imp.importSource}.d.ts`, dtsContent, { overwrite: true });
|
|
537
|
+
const fns = extractFunctionLikes(dtsFile);
|
|
538
|
+
const allNodeTypes = [];
|
|
539
|
+
const seenNames = new Set();
|
|
540
|
+
for (const fn of fns) {
|
|
541
|
+
const fnName = fn.getName();
|
|
542
|
+
if (!fnName)
|
|
543
|
+
continue;
|
|
544
|
+
// Skip duplicate function names (overloaded declarations in .d.ts)
|
|
545
|
+
if (seenNames.has(fnName))
|
|
546
|
+
continue;
|
|
547
|
+
seenNames.add(fnName);
|
|
548
|
+
const nodeType = this.inferNodeTypeFromFunction(fn, fnName, dtsPath);
|
|
549
|
+
nodeType.importSource = imp.importSource;
|
|
550
|
+
nodeType.functionText = undefined;
|
|
551
|
+
allNodeTypes.push(nodeType);
|
|
552
|
+
}
|
|
553
|
+
this.project.removeSourceFile(dtsFile);
|
|
554
|
+
// Cache all node types from this package
|
|
555
|
+
this.importCache.set(cacheKey, allNodeTypes);
|
|
556
|
+
// Find the specific function we need
|
|
557
|
+
const found = allNodeTypes.find((nt) => nt.functionName === imp.functionName);
|
|
558
|
+
if (found) {
|
|
559
|
+
return { ...found, name: imp.name, importSource: imp.importSource };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Silently skip packages whose .d.ts can't be parsed
|
|
564
|
+
}
|
|
565
|
+
return this.createImportStub(imp);
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Create a stub node type for @fwImport when proper inference fails.
|
|
569
|
+
* This provides graceful degradation rather than failing completely.
|
|
570
|
+
*/
|
|
571
|
+
createImportStub(imp) {
|
|
572
|
+
return {
|
|
573
|
+
type: 'NodeType',
|
|
574
|
+
name: imp.name,
|
|
575
|
+
functionName: imp.functionName,
|
|
576
|
+
importSource: imp.importSource,
|
|
577
|
+
variant: 'FUNCTION',
|
|
578
|
+
inputs: {},
|
|
579
|
+
outputs: { result: { dataType: 'ANY' } },
|
|
580
|
+
hasSuccessPort: true,
|
|
581
|
+
hasFailurePort: true,
|
|
582
|
+
executeWhen: 'CONJUNCTION',
|
|
583
|
+
isAsync: false,
|
|
584
|
+
// Mark as expression since most npm functions are pure
|
|
585
|
+
// This is a reasonable default for stubs
|
|
586
|
+
expression: true,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
extractNodeTypes(sourceFile, warnings) {
|
|
590
|
+
const nodeTypes = [];
|
|
591
|
+
extractFunctionLikes(sourceFile).forEach((fn) => {
|
|
592
|
+
// Parse JSDoc comments
|
|
593
|
+
const config = jsdocParser.parseNodeType(fn, warnings);
|
|
594
|
+
if (!config) {
|
|
595
|
+
const jsdocText = fn.getJsDocs().map((d) => d.getFullText()).join('');
|
|
596
|
+
if (jsdocText.includes('@flowWeaver') && jsdocText.includes('nodeType')) {
|
|
597
|
+
warnings.push(`Function "${fn.getName() || 'anonymous'}" has @flowWeaver annotation but could not be parsed. ` +
|
|
598
|
+
`Check for special characters (---) or malformed JSDoc syntax.`);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const functionName = fn.getName() || 'anonymous';
|
|
603
|
+
const nodeTypeName = config.name || functionName;
|
|
604
|
+
const inputs = {};
|
|
605
|
+
if (config.inputs) {
|
|
606
|
+
for (const [portName, portDef] of Object.entries(config.inputs)) {
|
|
607
|
+
inputs[portName] = {
|
|
608
|
+
dataType: portDef.type,
|
|
609
|
+
default: portDef.defaultValue,
|
|
610
|
+
optional: portDef.optional,
|
|
611
|
+
label: portDef.label,
|
|
612
|
+
expression: portDef.expression,
|
|
613
|
+
...(portDef.scope && { scope: portDef.scope }),
|
|
614
|
+
...(portDef.metadata && { metadata: portDef.metadata }),
|
|
615
|
+
...(portDef.tsType && { tsType: portDef.tsType }),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const outputs = {};
|
|
620
|
+
if (config.outputs) {
|
|
621
|
+
for (const [portName, portDef] of Object.entries(config.outputs)) {
|
|
622
|
+
outputs[portName] = {
|
|
623
|
+
dataType: portDef.type,
|
|
624
|
+
label: portDef.label,
|
|
625
|
+
...(portDef.scope && { scope: portDef.scope }),
|
|
626
|
+
...(portDef.metadata && { metadata: portDef.metadata }),
|
|
627
|
+
...(portDef.tsType && { tsType: portDef.tsType }),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Auto-infer ports for @expression nodes when @input/@output are missing.
|
|
632
|
+
// If the function has @expression but no explicit port annotations, infer
|
|
633
|
+
// data ports from the TypeScript function signature (same logic as unannotated functions).
|
|
634
|
+
if (config.expression) {
|
|
635
|
+
const hasExplicitDataInputs = Object.keys(inputs).some((k) => k !== 'execute');
|
|
636
|
+
const hasExplicitDataOutputs = Object.keys(outputs).some((k) => k !== 'onSuccess' && k !== 'onFailure');
|
|
637
|
+
if (!hasExplicitDataInputs || !hasExplicitDataOutputs) {
|
|
638
|
+
const inferred = this.inferNodeTypeFromFunction(fn, nodeTypeName, fn.getSourceFile().getFilePath());
|
|
639
|
+
if (!hasExplicitDataInputs) {
|
|
640
|
+
// Copy inferred data inputs (skip control flow ports)
|
|
641
|
+
for (const [portName, portDef] of Object.entries(inferred.inputs)) {
|
|
642
|
+
if (portName === 'execute')
|
|
643
|
+
continue;
|
|
644
|
+
inputs[portName] = portDef;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!hasExplicitDataOutputs) {
|
|
648
|
+
// Copy inferred data outputs (skip control flow ports)
|
|
649
|
+
for (const [portName, portDef] of Object.entries(inferred.outputs)) {
|
|
650
|
+
if (portName === 'onSuccess' || portName === 'onFailure')
|
|
651
|
+
continue;
|
|
652
|
+
outputs[portName] = portDef;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// ALL nodes must have execute input and onSuccess/onFailure outputs
|
|
658
|
+
// Execute port is visible by default so users can connect execution flow
|
|
659
|
+
// Merge user-defined ports with mandatory defaults to preserve special properties
|
|
660
|
+
inputs.execute = {
|
|
661
|
+
label: 'Execute', // Default label
|
|
662
|
+
...inputs.execute, // User can override label
|
|
663
|
+
dataType: 'STEP', // But dataType is mandatory
|
|
664
|
+
};
|
|
665
|
+
outputs.onSuccess = {
|
|
666
|
+
label: 'On Success', // Default label
|
|
667
|
+
...outputs.onSuccess, // User can override label
|
|
668
|
+
dataType: 'STEP', // But dataType is mandatory
|
|
669
|
+
isControlFlow: true, // Always a control flow port
|
|
670
|
+
};
|
|
671
|
+
outputs.onFailure = {
|
|
672
|
+
label: 'On Failure', // Default label
|
|
673
|
+
...outputs.onFailure, // User can override label
|
|
674
|
+
dataType: 'STEP', // But dataType is mandatory
|
|
675
|
+
failure: true, // Always a failure port
|
|
676
|
+
isControlFlow: true, // Always a control flow port
|
|
677
|
+
};
|
|
678
|
+
// Assign implicit port orders with mandatory port precedence
|
|
679
|
+
assignImplicitPortOrders(inputs);
|
|
680
|
+
assignImplicitPortOrders(outputs);
|
|
681
|
+
// Get function text (JSDoc comment + function)
|
|
682
|
+
// getText() returns just the function, so we need to prepend the JSDoc
|
|
683
|
+
const jsDocs = fn.getJsDocs();
|
|
684
|
+
const jsDocText = jsDocs.map((doc) => doc.getText()).join('\n');
|
|
685
|
+
const functionText = jsDocText ? `${jsDocText}\n${fn.getText()}` : fn.getText();
|
|
686
|
+
// Detect async keyword on function declaration
|
|
687
|
+
const isAsync = fn.isAsync();
|
|
688
|
+
// Convert defaultConfig
|
|
689
|
+
let defaultConfig = undefined;
|
|
690
|
+
if (config.defaultConfig) {
|
|
691
|
+
defaultConfig = {
|
|
692
|
+
label: config.defaultConfig.label,
|
|
693
|
+
description: config.defaultConfig.description,
|
|
694
|
+
pullExecution: config.defaultConfig.pullExecution,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
// Extract unique scope names from ports (per-port scoped architecture)
|
|
698
|
+
const portScopes = new Set();
|
|
699
|
+
Object.values(inputs).forEach((port) => {
|
|
700
|
+
if (port.scope)
|
|
701
|
+
portScopes.add(port.scope);
|
|
702
|
+
});
|
|
703
|
+
Object.values(outputs).forEach((port) => {
|
|
704
|
+
if (port.scope)
|
|
705
|
+
portScopes.add(port.scope);
|
|
706
|
+
});
|
|
707
|
+
// Determine scopes array:
|
|
708
|
+
// - If node has node-level scope: use that (old architecture)
|
|
709
|
+
// - Otherwise if ports have scopes: use unique port scopes (per-port architecture)
|
|
710
|
+
// - Otherwise: undefined (no scopes)
|
|
711
|
+
const scopes = config.scope
|
|
712
|
+
? [config.scope]
|
|
713
|
+
: portScopes.size > 0
|
|
714
|
+
? Array.from(portScopes)
|
|
715
|
+
: undefined;
|
|
716
|
+
nodeTypes.push({
|
|
717
|
+
type: 'NodeType',
|
|
718
|
+
name: nodeTypeName,
|
|
719
|
+
functionName,
|
|
720
|
+
variant: 'FUNCTION',
|
|
721
|
+
inputs,
|
|
722
|
+
outputs,
|
|
723
|
+
hasSuccessPort: RESERVED_PORT_NAMES.ON_SUCCESS in outputs,
|
|
724
|
+
hasFailurePort: RESERVED_PORT_NAMES.ON_FAILURE in outputs,
|
|
725
|
+
isAsync,
|
|
726
|
+
functionText,
|
|
727
|
+
executeWhen: config.executeWhen || EXECUTION_STRATEGIES.CONJUNCTION,
|
|
728
|
+
defaultConfig,
|
|
729
|
+
scope: config.scope,
|
|
730
|
+
scopes,
|
|
731
|
+
...(config.expression && { expression: true }),
|
|
732
|
+
...(fn.getDeclarationKind?.() && { declarationKind: fn.getDeclarationKind() }),
|
|
733
|
+
label: config.label,
|
|
734
|
+
description: config.description,
|
|
735
|
+
visuals: config.color || config.icon || config.tags
|
|
736
|
+
? {
|
|
737
|
+
color: config.color,
|
|
738
|
+
icon: config.icon,
|
|
739
|
+
tags: config.tags,
|
|
740
|
+
}
|
|
741
|
+
: undefined,
|
|
742
|
+
sourceLocation: {
|
|
743
|
+
file: sourceFile.getFilePath(),
|
|
744
|
+
line: fn.getStartLineNumber(false),
|
|
745
|
+
column: 0,
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
return nodeTypes;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Extract workflow signatures (metadata only) without validating instances.
|
|
753
|
+
* This enables same-file workflow invocation by making workflow ports available
|
|
754
|
+
* before the full workflow extraction pass.
|
|
755
|
+
*/
|
|
756
|
+
extractWorkflowSignatures(sourceFile, filePath, warnings) {
|
|
757
|
+
const workflows = [];
|
|
758
|
+
extractFunctionLikes(sourceFile).forEach((fn) => {
|
|
759
|
+
const config = jsdocParser.parseWorkflow(fn, warnings);
|
|
760
|
+
if (!config)
|
|
761
|
+
return;
|
|
762
|
+
const functionName = fn.getName() || 'anonymous';
|
|
763
|
+
const startPorts = this.parseStartPorts(fn, config);
|
|
764
|
+
const exitPorts = this.parseExitPorts(fn, config);
|
|
765
|
+
const userSpecifiedAsync = fn.isAsync();
|
|
766
|
+
workflows.push({
|
|
767
|
+
type: 'Workflow',
|
|
768
|
+
sourceFile: filePath,
|
|
769
|
+
name: config.name || functionName,
|
|
770
|
+
functionName,
|
|
771
|
+
nodeTypes: [],
|
|
772
|
+
instances: [],
|
|
773
|
+
connections: [],
|
|
774
|
+
startPorts,
|
|
775
|
+
exitPorts,
|
|
776
|
+
imports: [],
|
|
777
|
+
description: config.description,
|
|
778
|
+
userSpecifiedAsync,
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
return workflows;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Convert a workflow to a node type.
|
|
785
|
+
* This allows workflows to be used as nodes in other workflows.
|
|
786
|
+
*/
|
|
787
|
+
workflowToNodeType(workflow) {
|
|
788
|
+
return {
|
|
789
|
+
type: 'NodeType',
|
|
790
|
+
name: workflow.name,
|
|
791
|
+
functionName: workflow.functionName,
|
|
792
|
+
variant: 'IMPORTED_WORKFLOW',
|
|
793
|
+
path: workflow.sourceFile,
|
|
794
|
+
inputs: { ...workflow.startPorts },
|
|
795
|
+
outputs: { ...workflow.exitPorts },
|
|
796
|
+
hasSuccessPort: 'onSuccess' in workflow.exitPorts,
|
|
797
|
+
hasFailurePort: 'onFailure' in workflow.exitPorts,
|
|
798
|
+
isAsync: workflow.userSpecifiedAsync || false,
|
|
799
|
+
executeWhen: EXECUTION_STRATEGIES.CONJUNCTION,
|
|
800
|
+
description: workflow.description,
|
|
801
|
+
sourceLocation: {
|
|
802
|
+
file: workflow.sourceFile,
|
|
803
|
+
line: 0,
|
|
804
|
+
column: 0,
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
extractWorkflows(sourceFile, availableNodeTypes, filePath, errors, warnings) {
|
|
809
|
+
const workflows = [];
|
|
810
|
+
const allFunctions = extractFunctionLikes(sourceFile);
|
|
811
|
+
// Collect all function names in the file for unannotated-function hints in validator
|
|
812
|
+
const allFunctionNames = allFunctions
|
|
813
|
+
.map((fn) => fn.getName())
|
|
814
|
+
.filter((name) => !!name);
|
|
815
|
+
allFunctions.forEach((fn) => {
|
|
816
|
+
// Parse JSDoc comments
|
|
817
|
+
const config = jsdocParser.parseWorkflow(fn, warnings);
|
|
818
|
+
if (!config) {
|
|
819
|
+
const jsdocText = fn.getJsDocs().map((d) => d.getFullText()).join('');
|
|
820
|
+
if (jsdocText.includes('@flowWeaver') && jsdocText.includes('workflow')) {
|
|
821
|
+
warnings.push(`Function "${fn.getName() || 'anonymous'}" has @flowWeaver annotation but could not be parsed. ` +
|
|
822
|
+
`Check for special characters (---) or malformed JSDoc syntax.`);
|
|
823
|
+
}
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const functionName = fn.getName() || 'anonymous';
|
|
827
|
+
// Validate no IN/OUT pseudo-nodes in workflows (they're only for patterns)
|
|
828
|
+
if (config.connections) {
|
|
829
|
+
for (const conn of config.connections) {
|
|
830
|
+
if (conn.from.node === 'IN' || conn.from.node === 'OUT') {
|
|
831
|
+
errors.push(`Workflow "${functionName}" uses "${conn.from.node}" pseudo-node which is only valid in patterns. Use "Start" or "Exit" instead.`);
|
|
832
|
+
}
|
|
833
|
+
if (conn.to.node === 'IN' || conn.to.node === 'OUT') {
|
|
834
|
+
errors.push(`Workflow "${functionName}" uses "${conn.to.node}" pseudo-node which is only valid in patterns. Use "Start" or "Exit" instead.`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Detect async keyword on workflow function declaration
|
|
839
|
+
const userSpecifiedAsync = fn.isAsync();
|
|
840
|
+
const startPorts = this.parseStartPorts(fn, config);
|
|
841
|
+
const exitPorts = this.parseExitPorts(fn, config);
|
|
842
|
+
// Convert @fwImport annotations to properly inferred node types
|
|
843
|
+
// These are persisted in JSDoc so they survive file re-parsing
|
|
844
|
+
// Uses the same inference logic as TS imports for consistency
|
|
845
|
+
const importedNpmNodeTypes = (config.imports || []).map((imp) => this.resolveImportAnnotation(imp, filePath, warnings));
|
|
846
|
+
// Combine available node types with imported npm types for validation
|
|
847
|
+
const allAvailableNodeTypes = [...availableNodeTypes, ...importedNpmNodeTypes];
|
|
848
|
+
// Convert instances to NodeInstanceAST
|
|
849
|
+
const instances = (config.instances || []).map((inst) => {
|
|
850
|
+
// Validate node type exists — push error instead of throwing so that
|
|
851
|
+
// partial parse results remain usable (defense-in-depth for race conditions)
|
|
852
|
+
const nodeTypeExists = allAvailableNodeTypes.some((nt) => nt.name === inst.type || nt.functionName === inst.type);
|
|
853
|
+
if (!nodeTypeExists) {
|
|
854
|
+
errors.push(`Node type "${inst.type}" not found in workflow "${functionName}". ` +
|
|
855
|
+
`Available types: ${allAvailableNodeTypes.map((nt) => nt.functionName).join(', ') || '(none)'}`);
|
|
856
|
+
}
|
|
857
|
+
const position = config.positions?.[inst.id];
|
|
858
|
+
// Convert parentScope string "nodeName.scope" to parent object
|
|
859
|
+
let parent;
|
|
860
|
+
if (inst.parentScope) {
|
|
861
|
+
const dotIndex = inst.parentScope.indexOf('.');
|
|
862
|
+
if (dotIndex > 0) {
|
|
863
|
+
parent = {
|
|
864
|
+
id: inst.parentScope.substring(0, dotIndex),
|
|
865
|
+
scope: inst.parentScope.substring(dotIndex + 1),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// portConfigs are direction-agnostic (annotations don't carry direction info).
|
|
870
|
+
// Matching code handles undefined direction by matching any direction.
|
|
871
|
+
const portConfigs = inst.portConfigs;
|
|
872
|
+
return {
|
|
873
|
+
type: 'NodeInstance',
|
|
874
|
+
id: inst.id,
|
|
875
|
+
nodeType: inst.type,
|
|
876
|
+
...(parent && { parent }),
|
|
877
|
+
config: {
|
|
878
|
+
...(inst.label && { label: inst.label }),
|
|
879
|
+
...(position && { x: position.x, y: position.y }),
|
|
880
|
+
...(portConfigs && portConfigs.length > 0 && { portConfigs }),
|
|
881
|
+
...(inst.pullExecution && { pullExecution: inst.pullExecution }),
|
|
882
|
+
...(inst.minimized && { minimized: inst.minimized }),
|
|
883
|
+
...(inst.color && { color: inst.color }),
|
|
884
|
+
...(inst.icon && { icon: inst.icon }),
|
|
885
|
+
...(inst.tags && inst.tags.length > 0 && { tags: inst.tags }),
|
|
886
|
+
...(inst.width && { width: inst.width }),
|
|
887
|
+
...(inst.height && { height: inst.height }),
|
|
888
|
+
},
|
|
889
|
+
...(inst.sourceLocation && {
|
|
890
|
+
sourceLocation: { file: filePath, ...inst.sourceLocation },
|
|
891
|
+
}),
|
|
892
|
+
};
|
|
893
|
+
});
|
|
894
|
+
// Convert connections to ConnectionAST
|
|
895
|
+
const connections = (config.connections || []).map((conn) => ({
|
|
896
|
+
type: 'Connection',
|
|
897
|
+
from: conn.from,
|
|
898
|
+
to: conn.to,
|
|
899
|
+
...(conn.sourceLocation && { sourceLocation: { file: filePath, ...conn.sourceLocation } }),
|
|
900
|
+
}));
|
|
901
|
+
// Auto-connect: when @autoConnect is set and no explicit @connect annotations exist,
|
|
902
|
+
// auto-wire linear connections between nodes in declaration order
|
|
903
|
+
if (config.autoConnect && connections.length === 0 && instances.length > 0) {
|
|
904
|
+
const autoConnections = this.generateAutoConnections(instances, allAvailableNodeTypes, startPorts, exitPorts);
|
|
905
|
+
connections.push(...autoConnections);
|
|
906
|
+
}
|
|
907
|
+
// Expand @map macros into synthetic node types, instances, connections, and scopes
|
|
908
|
+
const scopes = config.scopes || {};
|
|
909
|
+
const macros = [];
|
|
910
|
+
if (config.maps && config.maps.length > 0) {
|
|
911
|
+
for (const mapConfig of config.maps) {
|
|
912
|
+
this.expandMapMacro(mapConfig, instances, connections, scopes, allAvailableNodeTypes, macros, errors, warnings);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// Expand @path macros into multi-step execution routes with scope walking
|
|
916
|
+
if (config.paths && config.paths.length > 0) {
|
|
917
|
+
this.expandPathMacros(config.paths, instances, connections, allAvailableNodeTypes, startPorts, exitPorts, macros, errors, warnings);
|
|
918
|
+
}
|
|
919
|
+
// Include ALL available nodeTypes in the workflow AST, plus imported npm types.
|
|
920
|
+
// Previously this filtered to only nodeTypes used by instances, but that caused
|
|
921
|
+
// a bug: when creating a new nodeType and then adding its first instance,
|
|
922
|
+
// the second operation would re-parse the file, see no instances using the
|
|
923
|
+
// nodeType yet, filter it out, and then removeOrphanedNodeTypeFunctions would
|
|
924
|
+
// delete the nodeType function that was just written.
|
|
925
|
+
// NPM types come from @import annotations in JSDoc (persisted to survive re-parsing).
|
|
926
|
+
// Deduplicate: @fwImport types take precedence over external/runtime types with the same name.
|
|
927
|
+
// Without this, each parse+generate cycle adds one more duplicate @fwImport entry.
|
|
928
|
+
const importedNames = new Set(importedNpmNodeTypes.map((nt) => nt.name));
|
|
929
|
+
// Use allAvailableNodeTypes (includes synthetic MAP_ITERATOR types from @map macros)
|
|
930
|
+
const dedupedAvailableTypes = allAvailableNodeTypes.filter((nt) => !importedNames.has(nt.name));
|
|
931
|
+
const workflowNodeTypes = [...dedupedAvailableTypes, ...importedNpmNodeTypes];
|
|
932
|
+
// Extract Start/Exit positions from config.positions
|
|
933
|
+
const ui = {};
|
|
934
|
+
const startPosition = config.positions?.['Start'];
|
|
935
|
+
if (startPosition) {
|
|
936
|
+
ui.startNode = { x: startPosition.x, y: startPosition.y };
|
|
937
|
+
}
|
|
938
|
+
const exitPosition = config.positions?.['Exit'];
|
|
939
|
+
if (exitPosition) {
|
|
940
|
+
ui.exitNode = { x: exitPosition.x, y: exitPosition.y };
|
|
941
|
+
}
|
|
942
|
+
workflows.push({
|
|
943
|
+
type: 'Workflow',
|
|
944
|
+
sourceFile: filePath,
|
|
945
|
+
name: config.name || functionName,
|
|
946
|
+
functionName: functionName,
|
|
947
|
+
nodeTypes: workflowNodeTypes,
|
|
948
|
+
instances,
|
|
949
|
+
connections,
|
|
950
|
+
scopes,
|
|
951
|
+
startPorts,
|
|
952
|
+
exitPorts,
|
|
953
|
+
imports: [],
|
|
954
|
+
description: config.description,
|
|
955
|
+
userSpecifiedAsync,
|
|
956
|
+
availableFunctionNames: allFunctionNames,
|
|
957
|
+
...(macros.length > 0 && { macros }),
|
|
958
|
+
...(Object.keys(ui).length > 0 && { ui }),
|
|
959
|
+
...((config.strictTypes !== undefined || config.autoConnect ||
|
|
960
|
+
config.trigger || config.cancelOn || config.retries !== undefined ||
|
|
961
|
+
config.timeout || config.throttle) && {
|
|
962
|
+
options: {
|
|
963
|
+
...(config.strictTypes !== undefined && { strictTypes: config.strictTypes }),
|
|
964
|
+
...(config.autoConnect && { autoConnect: true }),
|
|
965
|
+
...(config.trigger && { trigger: config.trigger }),
|
|
966
|
+
...(config.cancelOn && { cancelOn: config.cancelOn }),
|
|
967
|
+
...(config.retries !== undefined && { retries: config.retries }),
|
|
968
|
+
...(config.timeout && { timeout: config.timeout }),
|
|
969
|
+
...(config.throttle && { throttle: config.throttle }),
|
|
970
|
+
},
|
|
971
|
+
}),
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
return workflows;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Extract patterns from a source file.
|
|
978
|
+
* Patterns are defined with @flowWeaver pattern annotation.
|
|
979
|
+
*/
|
|
980
|
+
extractPatterns(sourceFile, availableNodeTypes, filePath, errors, warnings) {
|
|
981
|
+
const patterns = [];
|
|
982
|
+
const seenNames = new Set();
|
|
983
|
+
extractFunctionLikes(sourceFile).forEach((fn) => {
|
|
984
|
+
// Parse JSDoc comments for pattern
|
|
985
|
+
const config = jsdocParser.parsePattern(fn, warnings);
|
|
986
|
+
if (!config) {
|
|
987
|
+
const jsdocText = fn.getJsDocs().map((d) => d.getFullText()).join('');
|
|
988
|
+
if (jsdocText.includes('@flowWeaver') && jsdocText.includes('pattern')) {
|
|
989
|
+
warnings.push(`Function "${fn.getName() || 'anonymous'}" has @flowWeaver annotation but could not be parsed. ` +
|
|
990
|
+
`Check for special characters (---) or malformed JSDoc syntax.`);
|
|
991
|
+
}
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
// Validate required @name
|
|
995
|
+
if (!config.name) {
|
|
996
|
+
errors.push(`Pattern is missing required @name tag in function "${fn.getName()}"`);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
// Check for duplicate names
|
|
1000
|
+
if (seenNames.has(config.name)) {
|
|
1001
|
+
errors.push(`Duplicate pattern name "${config.name}" in file`);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
seenNames.add(config.name);
|
|
1005
|
+
// Extract node types used by this pattern
|
|
1006
|
+
const patternNodeTypes = availableNodeTypes.filter((nt) => config.instances?.some((inst) => inst.nodeType === nt.name));
|
|
1007
|
+
// Build connections from config
|
|
1008
|
+
const connections = (config.connections || []).map((conn) => ({
|
|
1009
|
+
type: 'Connection',
|
|
1010
|
+
from: conn.from,
|
|
1011
|
+
to: conn.to,
|
|
1012
|
+
}));
|
|
1013
|
+
// Extract input/output ports from @port declarations
|
|
1014
|
+
const inputPorts = {};
|
|
1015
|
+
const outputPorts = {};
|
|
1016
|
+
if (config.ports) {
|
|
1017
|
+
for (const port of config.ports) {
|
|
1018
|
+
if (port.direction === 'IN') {
|
|
1019
|
+
inputPorts[port.name] = { description: port.description };
|
|
1020
|
+
}
|
|
1021
|
+
else if (port.direction === 'OUT') {
|
|
1022
|
+
outputPorts[port.name] = { description: port.description };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// Build instances from config
|
|
1027
|
+
const instances = (config.instances || []).map((inst) => ({
|
|
1028
|
+
type: 'NodeInstance',
|
|
1029
|
+
id: inst.id,
|
|
1030
|
+
nodeType: inst.nodeType,
|
|
1031
|
+
config: inst.config || {},
|
|
1032
|
+
}));
|
|
1033
|
+
patterns.push({
|
|
1034
|
+
type: 'Pattern',
|
|
1035
|
+
sourceFile: filePath,
|
|
1036
|
+
name: config.name,
|
|
1037
|
+
description: config.description,
|
|
1038
|
+
nodeTypes: patternNodeTypes,
|
|
1039
|
+
instances,
|
|
1040
|
+
connections,
|
|
1041
|
+
inputPorts,
|
|
1042
|
+
outputPorts,
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
return patterns;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Infer a TNodeTypeAST from a single function's TypeScript signature.
|
|
1049
|
+
* Shared helper used by both same-file and cross-file inference.
|
|
1050
|
+
*/
|
|
1051
|
+
inferNodeTypeFromFunction(fn, name, filePath) {
|
|
1052
|
+
// Infer inputs from parameters
|
|
1053
|
+
const inputs = {};
|
|
1054
|
+
const params = fn.getParameters();
|
|
1055
|
+
const firstParamIsExecute = params.length > 0 && params[0].getName() === 'execute';
|
|
1056
|
+
for (const param of params) {
|
|
1057
|
+
const paramName = param.getName();
|
|
1058
|
+
const tsType = param.getType().getText(param);
|
|
1059
|
+
const dataType = inferDataTypeFromTS(tsType);
|
|
1060
|
+
const optional = param.isOptional() || param.hasInitializer();
|
|
1061
|
+
inputs[paramName] = {
|
|
1062
|
+
dataType,
|
|
1063
|
+
optional: optional || undefined,
|
|
1064
|
+
label: this.capitalize(paramName),
|
|
1065
|
+
tsType,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
// Infer outputs from return type
|
|
1069
|
+
const outputs = {};
|
|
1070
|
+
let returnType = fn.getReturnType();
|
|
1071
|
+
const returnTypeText = returnType.getText();
|
|
1072
|
+
// Unwrap Promise<T>
|
|
1073
|
+
if (returnTypeText.startsWith('Promise<')) {
|
|
1074
|
+
const typeArgs = returnType.getTypeArguments();
|
|
1075
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
1076
|
+
returnType = typeArgs[0];
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const unwrappedText = returnType.getText();
|
|
1080
|
+
if (unwrappedText !== 'void' && unwrappedText !== 'undefined') {
|
|
1081
|
+
const primitiveTypes = new Set(['string', 'number', 'boolean', 'any', 'unknown', 'never']);
|
|
1082
|
+
const isPrimitive = primitiveTypes.has(unwrappedText);
|
|
1083
|
+
const isArray = unwrappedText.endsWith('[]') || unwrappedText.startsWith('Array<');
|
|
1084
|
+
const properties = returnType.getProperties();
|
|
1085
|
+
const isObjectLike = !isPrimitive && !isArray && returnType.isObject() && properties.length > 0;
|
|
1086
|
+
if (isObjectLike) {
|
|
1087
|
+
for (const prop of properties) {
|
|
1088
|
+
const propName = prop.getName();
|
|
1089
|
+
if (propName === 'onSuccess' || propName === 'onFailure')
|
|
1090
|
+
continue;
|
|
1091
|
+
const propType = prop.getTypeAtLocation(fn.getTypeResolutionNode());
|
|
1092
|
+
const propTypeText = propType.getText();
|
|
1093
|
+
const dataType = inferDataTypeFromTS(propTypeText);
|
|
1094
|
+
outputs[propName] = {
|
|
1095
|
+
dataType,
|
|
1096
|
+
label: this.capitalize(propName),
|
|
1097
|
+
tsType: propTypeText,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
const dataType = inferDataTypeFromTS(unwrappedText);
|
|
1103
|
+
outputs.result = {
|
|
1104
|
+
dataType,
|
|
1105
|
+
label: 'Result',
|
|
1106
|
+
tsType: unwrappedText,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Add mandatory ports
|
|
1111
|
+
inputs.execute = { dataType: 'STEP', label: 'Execute' };
|
|
1112
|
+
outputs.onSuccess = { dataType: 'STEP', label: 'On Success', isControlFlow: true };
|
|
1113
|
+
outputs.onFailure = {
|
|
1114
|
+
dataType: 'STEP',
|
|
1115
|
+
label: 'On Failure',
|
|
1116
|
+
failure: true,
|
|
1117
|
+
isControlFlow: true,
|
|
1118
|
+
};
|
|
1119
|
+
// Assign implicit port orders
|
|
1120
|
+
assignImplicitPortOrders(inputs);
|
|
1121
|
+
assignImplicitPortOrders(outputs);
|
|
1122
|
+
// Build TNodeTypeAST
|
|
1123
|
+
const jsDocs = fn.getJsDocs();
|
|
1124
|
+
const jsDocText = jsDocs.map((doc) => doc.getText()).join('\n');
|
|
1125
|
+
const functionText = jsDocText ? `${jsDocText}\n${fn.getText()}` : fn.getText();
|
|
1126
|
+
return {
|
|
1127
|
+
type: 'NodeType',
|
|
1128
|
+
name,
|
|
1129
|
+
functionName: name,
|
|
1130
|
+
variant: 'FUNCTION',
|
|
1131
|
+
inputs,
|
|
1132
|
+
outputs,
|
|
1133
|
+
hasSuccessPort: true,
|
|
1134
|
+
hasFailurePort: true,
|
|
1135
|
+
isAsync: fn.isAsync() || returnTypeText.startsWith('Promise<'),
|
|
1136
|
+
executeWhen: EXECUTION_STRATEGIES.CONJUNCTION,
|
|
1137
|
+
expression: !firstParamIsExecute, // Expression only if original function lacks execute as first param
|
|
1138
|
+
inferred: true,
|
|
1139
|
+
functionText,
|
|
1140
|
+
...(fn.getDeclarationKind?.() && {
|
|
1141
|
+
declarationKind: fn.getDeclarationKind(),
|
|
1142
|
+
}),
|
|
1143
|
+
sourceLocation: {
|
|
1144
|
+
file: filePath,
|
|
1145
|
+
line: fn.getStartLineNumber(false),
|
|
1146
|
+
column: 0,
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Pre-infer ALL unannotated functions from a source file.
|
|
1152
|
+
* Used for imported files so the named-import filter can scope them.
|
|
1153
|
+
*/
|
|
1154
|
+
inferAllUnannotatedFunctions(sourceFile, existingNodeTypes) {
|
|
1155
|
+
const allFunctions = extractFunctionLikes(sourceFile);
|
|
1156
|
+
const existingNames = new Set();
|
|
1157
|
+
for (const nt of existingNodeTypes) {
|
|
1158
|
+
existingNames.add(nt.name);
|
|
1159
|
+
existingNames.add(nt.functionName);
|
|
1160
|
+
}
|
|
1161
|
+
const inferred = [];
|
|
1162
|
+
for (const fn of allFunctions) {
|
|
1163
|
+
const fnName = fn.getName();
|
|
1164
|
+
if (!fnName)
|
|
1165
|
+
continue;
|
|
1166
|
+
// Skip if already known (annotated or from another source)
|
|
1167
|
+
if (existingNames.has(fnName))
|
|
1168
|
+
continue;
|
|
1169
|
+
// Must NOT have a valid @flowWeaver annotation
|
|
1170
|
+
if (this.hasFlowWeaverAnnotation(fn))
|
|
1171
|
+
continue;
|
|
1172
|
+
inferred.push(this.inferNodeTypeFromFunction(fn, fnName, sourceFile.getFilePath()));
|
|
1173
|
+
existingNames.add(fnName);
|
|
1174
|
+
}
|
|
1175
|
+
return inferred;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Auto-infer node types from unannotated functions referenced by @node.
|
|
1179
|
+
*
|
|
1180
|
+
* When a workflow references a function via @node that has no @flowWeaver
|
|
1181
|
+
* nodeType annotation, we infer an expression node type from its TypeScript
|
|
1182
|
+
* signature. Phase 1: same-file functions only.
|
|
1183
|
+
*/
|
|
1184
|
+
inferNodeTypesFromUnannotated(sourceFile, existingNodeTypes) {
|
|
1185
|
+
const allFunctions = extractFunctionLikes(sourceFile);
|
|
1186
|
+
// 1. Pre-scan workflows for @node references to collect referenced type names
|
|
1187
|
+
const referencedTypes = new Set();
|
|
1188
|
+
for (const fn of allFunctions) {
|
|
1189
|
+
const config = jsdocParser.parseWorkflow(fn, []);
|
|
1190
|
+
if (!config)
|
|
1191
|
+
continue;
|
|
1192
|
+
for (const inst of config.instances || []) {
|
|
1193
|
+
referencedTypes.add(inst.type);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
// 2. Find unresolved types: referenced but not in existingNodeTypes
|
|
1197
|
+
const existingNames = new Set();
|
|
1198
|
+
for (const nt of existingNodeTypes) {
|
|
1199
|
+
existingNames.add(nt.name);
|
|
1200
|
+
existingNames.add(nt.functionName);
|
|
1201
|
+
}
|
|
1202
|
+
const unresolvedTypes = new Set();
|
|
1203
|
+
for (const typeName of referencedTypes) {
|
|
1204
|
+
if (!existingNames.has(typeName)) {
|
|
1205
|
+
unresolvedTypes.add(typeName);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (unresolvedTypes.size === 0)
|
|
1209
|
+
return [];
|
|
1210
|
+
// 3. Match unresolved types to unannotated functions in the same file
|
|
1211
|
+
const inferredNodeTypes = [];
|
|
1212
|
+
const alreadyInferred = new Set();
|
|
1213
|
+
for (const unresolvedType of unresolvedTypes) {
|
|
1214
|
+
if (alreadyInferred.has(unresolvedType))
|
|
1215
|
+
continue;
|
|
1216
|
+
// Find matching function
|
|
1217
|
+
const matchedFn = allFunctions.find((fn) => {
|
|
1218
|
+
if (fn.getName() !== unresolvedType)
|
|
1219
|
+
return false;
|
|
1220
|
+
// Must NOT have a valid @flowWeaver annotation
|
|
1221
|
+
return !this.hasFlowWeaverAnnotation(fn);
|
|
1222
|
+
});
|
|
1223
|
+
if (!matchedFn)
|
|
1224
|
+
continue;
|
|
1225
|
+
inferredNodeTypes.push(this.inferNodeTypeFromFunction(matchedFn, unresolvedType, sourceFile.getFilePath()));
|
|
1226
|
+
alreadyInferred.add(unresolvedType);
|
|
1227
|
+
}
|
|
1228
|
+
return inferredNodeTypes;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Expand a @map macro into synthetic node type, instances, connections, and scope.
|
|
1232
|
+
*
|
|
1233
|
+
* @map loop proc over scan.files
|
|
1234
|
+
*
|
|
1235
|
+
* Expands to:
|
|
1236
|
+
* - A synthetic MAP_ITERATOR node type for "loop" with scoped ports
|
|
1237
|
+
* - An instance "loop" of that synthetic type
|
|
1238
|
+
* - The child instance "proc" placed inside loop.iterate scope
|
|
1239
|
+
* - All scoped connections auto-generated
|
|
1240
|
+
* - Upstream connection from scan.files -> loop.items
|
|
1241
|
+
*/
|
|
1242
|
+
expandMapMacro(mapConfig, instances, connections, scopes, availableNodeTypes, macros, errors, _warnings) {
|
|
1243
|
+
const { instanceId, childId, sourceNode, sourcePort } = mapConfig;
|
|
1244
|
+
const scopeName = 'iterate';
|
|
1245
|
+
// Find the child node instance (must already be declared via @node)
|
|
1246
|
+
const childInstance = instances.find((inst) => inst.id === childId);
|
|
1247
|
+
if (!childInstance) {
|
|
1248
|
+
errors.push(`@map "${instanceId}": child node "${childId}" not found. Declare it with @node before using @map.`);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
// Find the child's node type to determine ports
|
|
1252
|
+
const childNodeType = availableNodeTypes.find((nt) => nt.name === childInstance.nodeType || nt.functionName === childInstance.nodeType);
|
|
1253
|
+
if (!childNodeType) {
|
|
1254
|
+
errors.push(`@map "${instanceId}": node type "${childInstance.nodeType}" for child "${childId}" not found.`);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
// Determine input/output ports on the child
|
|
1258
|
+
// Auto-infer: first non-execute data input, first non-control-flow data output
|
|
1259
|
+
let inputPort = mapConfig.inputPort;
|
|
1260
|
+
let outputPort = mapConfig.outputPort;
|
|
1261
|
+
if (!inputPort) {
|
|
1262
|
+
const dataInputs = Object.entries(childNodeType.inputs).filter(([name, def]) => name !== 'execute' && def.dataType !== 'STEP');
|
|
1263
|
+
if (dataInputs.length === 0) {
|
|
1264
|
+
errors.push(`@map "${instanceId}": child node "${childId}" has no data input ports to receive items.`);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
inputPort = dataInputs[0][0];
|
|
1268
|
+
}
|
|
1269
|
+
if (!outputPort) {
|
|
1270
|
+
const dataOutputs = Object.entries(childNodeType.outputs).filter(([name, def]) => name !== 'onSuccess' && name !== 'onFailure' && def.dataType !== 'STEP' && !def.isControlFlow && !def.failure);
|
|
1271
|
+
if (dataOutputs.length === 0) {
|
|
1272
|
+
errors.push(`@map "${instanceId}": child node "${childId}" has no data output ports for results.`);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
outputPort = dataOutputs[0][0];
|
|
1276
|
+
}
|
|
1277
|
+
// Get type info from child ports for the synthetic type
|
|
1278
|
+
const childInputDef = childNodeType.inputs[inputPort];
|
|
1279
|
+
const childOutputDef = childNodeType.outputs[outputPort];
|
|
1280
|
+
// Create synthetic MAP_ITERATOR node type
|
|
1281
|
+
const syntheticTypeName = `__map_${instanceId}__`;
|
|
1282
|
+
const syntheticNodeType = {
|
|
1283
|
+
type: 'NodeType',
|
|
1284
|
+
name: syntheticTypeName,
|
|
1285
|
+
functionName: syntheticTypeName,
|
|
1286
|
+
variant: 'MAP_ITERATOR',
|
|
1287
|
+
isAsync: true,
|
|
1288
|
+
executeWhen: 'CONJUNCTION',
|
|
1289
|
+
hasSuccessPort: true,
|
|
1290
|
+
hasFailurePort: true,
|
|
1291
|
+
scope: scopeName,
|
|
1292
|
+
scopes: [scopeName],
|
|
1293
|
+
inputs: {
|
|
1294
|
+
execute: { dataType: 'STEP', label: 'Execute' },
|
|
1295
|
+
items: {
|
|
1296
|
+
dataType: 'ARRAY',
|
|
1297
|
+
label: 'Items',
|
|
1298
|
+
tsType: childInputDef?.tsType ? `(${childInputDef.tsType})[]` : 'unknown[]',
|
|
1299
|
+
},
|
|
1300
|
+
// Scoped INPUT ports (receive from children)
|
|
1301
|
+
success: { dataType: 'STEP', scope: scopeName },
|
|
1302
|
+
failure: { dataType: 'STEP', scope: scopeName },
|
|
1303
|
+
processed: {
|
|
1304
|
+
dataType: childOutputDef?.dataType || 'ANY',
|
|
1305
|
+
scope: scopeName,
|
|
1306
|
+
...(childOutputDef?.tsType && { tsType: childOutputDef.tsType }),
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
outputs: {
|
|
1310
|
+
onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
|
|
1311
|
+
onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
|
|
1312
|
+
results: {
|
|
1313
|
+
dataType: 'ARRAY',
|
|
1314
|
+
label: 'Results',
|
|
1315
|
+
tsType: childOutputDef?.tsType ? `(${childOutputDef.tsType})[]` : 'unknown[]',
|
|
1316
|
+
},
|
|
1317
|
+
// Scoped OUTPUT ports (send to children)
|
|
1318
|
+
start: { dataType: 'STEP', scope: scopeName },
|
|
1319
|
+
item: {
|
|
1320
|
+
dataType: childInputDef?.dataType || 'ANY',
|
|
1321
|
+
scope: scopeName,
|
|
1322
|
+
...(childInputDef?.tsType && { tsType: childInputDef.tsType }),
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
};
|
|
1326
|
+
// Add synthetic type to available types
|
|
1327
|
+
availableNodeTypes.push(syntheticNodeType);
|
|
1328
|
+
// Create instance for the map iterator
|
|
1329
|
+
const mapInstance = {
|
|
1330
|
+
type: 'NodeInstance',
|
|
1331
|
+
id: instanceId,
|
|
1332
|
+
nodeType: syntheticTypeName,
|
|
1333
|
+
};
|
|
1334
|
+
instances.push(mapInstance);
|
|
1335
|
+
// Move child instance into the scope
|
|
1336
|
+
childInstance.parent = { id: instanceId, scope: scopeName };
|
|
1337
|
+
// Generate scoped connections
|
|
1338
|
+
// loop.start:iterate -> proc.execute
|
|
1339
|
+
connections.push({
|
|
1340
|
+
type: 'Connection',
|
|
1341
|
+
from: { node: instanceId, port: 'start', scope: scopeName },
|
|
1342
|
+
to: { node: childId, port: 'execute', scope: scopeName },
|
|
1343
|
+
});
|
|
1344
|
+
// loop.item:iterate -> proc.<inputPort>
|
|
1345
|
+
connections.push({
|
|
1346
|
+
type: 'Connection',
|
|
1347
|
+
from: { node: instanceId, port: 'item', scope: scopeName },
|
|
1348
|
+
to: { node: childId, port: inputPort, scope: scopeName },
|
|
1349
|
+
});
|
|
1350
|
+
// proc.<outputPort> -> loop.processed:iterate
|
|
1351
|
+
connections.push({
|
|
1352
|
+
type: 'Connection',
|
|
1353
|
+
from: { node: childId, port: outputPort, scope: scopeName },
|
|
1354
|
+
to: { node: instanceId, port: 'processed', scope: scopeName },
|
|
1355
|
+
});
|
|
1356
|
+
// proc.onSuccess -> loop.success:iterate
|
|
1357
|
+
connections.push({
|
|
1358
|
+
type: 'Connection',
|
|
1359
|
+
from: { node: childId, port: 'onSuccess', scope: scopeName },
|
|
1360
|
+
to: { node: instanceId, port: 'success', scope: scopeName },
|
|
1361
|
+
});
|
|
1362
|
+
// proc.onFailure -> loop.failure:iterate
|
|
1363
|
+
connections.push({
|
|
1364
|
+
type: 'Connection',
|
|
1365
|
+
from: { node: childId, port: 'onFailure', scope: scopeName },
|
|
1366
|
+
to: { node: instanceId, port: 'failure', scope: scopeName },
|
|
1367
|
+
});
|
|
1368
|
+
// Generate upstream connection: source.port -> loop.items
|
|
1369
|
+
connections.push({
|
|
1370
|
+
type: 'Connection',
|
|
1371
|
+
from: { node: sourceNode, port: sourcePort },
|
|
1372
|
+
to: { node: instanceId, port: 'items' },
|
|
1373
|
+
});
|
|
1374
|
+
// Register scope
|
|
1375
|
+
scopes[`${instanceId}.${scopeName}`] = [childId];
|
|
1376
|
+
// Store macro for round-trip preservation
|
|
1377
|
+
macros.push({
|
|
1378
|
+
type: 'map',
|
|
1379
|
+
instanceId,
|
|
1380
|
+
childId,
|
|
1381
|
+
sourcePort: `${sourceNode}.${sourcePort}`,
|
|
1382
|
+
...(mapConfig.inputPort && { inputPort: mapConfig.inputPort }),
|
|
1383
|
+
...(mapConfig.outputPort && { outputPort: mapConfig.outputPort }),
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Expand @path macros into multi-step execution routes with scope walking.
|
|
1388
|
+
* Processes all paths together for shared deduplication.
|
|
1389
|
+
*/
|
|
1390
|
+
expandPathMacros(pathConfigs, instances, connections, availableNodeTypes, startPorts, exitPorts, macros, errors, warnings) {
|
|
1391
|
+
// Helper to find a node type by name or functionName
|
|
1392
|
+
const findNodeType = (nodeTypeName) => availableNodeTypes.find((nt) => nt.name === nodeTypeName || nt.functionName === nodeTypeName);
|
|
1393
|
+
// Helper to resolve instance → node type
|
|
1394
|
+
const getNodeType = (nodeId) => {
|
|
1395
|
+
const instance = instances.find((inst) => inst.id === nodeId);
|
|
1396
|
+
if (!instance)
|
|
1397
|
+
return undefined;
|
|
1398
|
+
return findNodeType(instance.nodeType);
|
|
1399
|
+
};
|
|
1400
|
+
// Helper to get output ports for a node (handling Start/Exit)
|
|
1401
|
+
const getOutputPorts = (nodeId) => {
|
|
1402
|
+
if (nodeId === 'Start')
|
|
1403
|
+
return startPorts;
|
|
1404
|
+
if (nodeId === 'Exit')
|
|
1405
|
+
return exitPorts;
|
|
1406
|
+
const nodeType = getNodeType(nodeId);
|
|
1407
|
+
return nodeType?.outputs || {};
|
|
1408
|
+
};
|
|
1409
|
+
// Helper to get input ports for a node (handling Start/Exit)
|
|
1410
|
+
const getInputPorts = (nodeId) => {
|
|
1411
|
+
if (nodeId === 'Exit')
|
|
1412
|
+
return exitPorts;
|
|
1413
|
+
if (nodeId === 'Start')
|
|
1414
|
+
return startPorts;
|
|
1415
|
+
const nodeType = getNodeType(nodeId);
|
|
1416
|
+
return nodeType?.inputs || {};
|
|
1417
|
+
};
|
|
1418
|
+
// Build a set of existing connection keys for deduplication
|
|
1419
|
+
const existingKeys = new Set();
|
|
1420
|
+
for (const conn of connections) {
|
|
1421
|
+
existingKeys.add(`${conn.from.node}.${conn.from.port}->${conn.to.node}.${conn.to.port}`);
|
|
1422
|
+
}
|
|
1423
|
+
const addConnection = (fromNode, fromPort, toNode, toPort) => {
|
|
1424
|
+
const key = `${fromNode}.${fromPort}->${toNode}.${toPort}`;
|
|
1425
|
+
if (existingKeys.has(key))
|
|
1426
|
+
return;
|
|
1427
|
+
existingKeys.add(key);
|
|
1428
|
+
connections.push({
|
|
1429
|
+
type: 'Connection',
|
|
1430
|
+
from: { node: fromNode, port: fromPort },
|
|
1431
|
+
to: { node: toNode, port: toPort },
|
|
1432
|
+
});
|
|
1433
|
+
};
|
|
1434
|
+
for (const pathConfig of pathConfigs) {
|
|
1435
|
+
const { steps } = pathConfig;
|
|
1436
|
+
if (steps.length < 2) {
|
|
1437
|
+
errors.push(`@path requires at least 2 steps, got ${steps.length}.`);
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
// Validate all node references exist
|
|
1441
|
+
let valid = true;
|
|
1442
|
+
for (const step of steps) {
|
|
1443
|
+
if (step.node === 'Start' || step.node === 'Exit')
|
|
1444
|
+
continue;
|
|
1445
|
+
const instance = instances.find((inst) => inst.id === step.node);
|
|
1446
|
+
if (!instance) {
|
|
1447
|
+
errors.push(`@path: node "${step.node}" not found. Declare it with @node before using @path.`);
|
|
1448
|
+
valid = false;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (!valid)
|
|
1452
|
+
continue;
|
|
1453
|
+
// Generate connections for each consecutive pair
|
|
1454
|
+
for (let i = 0; i < steps.length - 1; i++) {
|
|
1455
|
+
const current = steps[i];
|
|
1456
|
+
const next = steps[i + 1];
|
|
1457
|
+
const currentId = current.node;
|
|
1458
|
+
const nextId = next.node;
|
|
1459
|
+
const route = current.route || 'ok';
|
|
1460
|
+
// Control flow connection
|
|
1461
|
+
if (currentId === 'Start') {
|
|
1462
|
+
addConnection('Start', 'execute', nextId, 'execute');
|
|
1463
|
+
}
|
|
1464
|
+
else if (nextId === 'Exit') {
|
|
1465
|
+
if (route === 'fail') {
|
|
1466
|
+
addConnection(currentId, 'onFailure', 'Exit', 'onFailure');
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
addConnection(currentId, 'onSuccess', 'Exit', 'onSuccess');
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
else {
|
|
1473
|
+
if (route === 'fail') {
|
|
1474
|
+
addConnection(currentId, 'onFailure', nextId, 'execute');
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
addConnection(currentId, 'onSuccess', nextId, 'execute');
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
// Data port scope walking (skip Exit — no data inputs to wire)
|
|
1481
|
+
if (nextId === 'Exit')
|
|
1482
|
+
continue;
|
|
1483
|
+
const nextInputs = getInputPorts(nextId);
|
|
1484
|
+
for (const [inputName] of Object.entries(nextInputs)) {
|
|
1485
|
+
if (isControlFlowPort(inputName))
|
|
1486
|
+
continue;
|
|
1487
|
+
// Walk backward through path steps to find nearest ancestor with same-name output
|
|
1488
|
+
for (let j = i; j >= 0; j--) {
|
|
1489
|
+
const ancestorId = steps[j].node;
|
|
1490
|
+
const ancestorOutputs = getOutputPorts(ancestorId);
|
|
1491
|
+
if (inputName in ancestorOutputs && !isControlFlowPort(inputName)) {
|
|
1492
|
+
addConnection(ancestorId, inputName, nextId, inputName);
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
// Store macro for round-trip preservation
|
|
1499
|
+
macros.push({
|
|
1500
|
+
type: 'path',
|
|
1501
|
+
steps: steps.map(s => s.route ? { node: s.node, route: s.route } : { node: s.node }),
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Generate automatic connections for @autoConnect workflows.
|
|
1507
|
+
* Wires nodes in declaration order as a linear pipeline:
|
|
1508
|
+
* Start -> first node -> second node -> ... -> last node -> Exit
|
|
1509
|
+
*
|
|
1510
|
+
* For each consecutive pair:
|
|
1511
|
+
* - Connect execute flow: previous.onSuccess -> next.execute
|
|
1512
|
+
* - Connect data ports where output name matches input name
|
|
1513
|
+
* For first node: Start.execute -> first.execute + match Start data ports to first inputs
|
|
1514
|
+
* For last node: last.onSuccess -> Exit.execute + match last outputs to Exit ports
|
|
1515
|
+
*/
|
|
1516
|
+
generateAutoConnections(instances, availableNodeTypes, startPorts, exitPorts) {
|
|
1517
|
+
const connections = [];
|
|
1518
|
+
// Helper to find a node type by name or functionName
|
|
1519
|
+
const findNodeType = (nodeTypeName) => availableNodeTypes.find((nt) => nt.name === nodeTypeName || nt.functionName === nodeTypeName);
|
|
1520
|
+
// Connect Start -> first node
|
|
1521
|
+
if (instances.length > 0) {
|
|
1522
|
+
const firstInstance = instances[0];
|
|
1523
|
+
const firstNodeType = findNodeType(firstInstance.nodeType);
|
|
1524
|
+
// Start.execute -> first.execute (execution flow)
|
|
1525
|
+
connections.push({
|
|
1526
|
+
type: 'Connection',
|
|
1527
|
+
from: { node: 'Start', port: 'execute' },
|
|
1528
|
+
to: { node: firstInstance.id, port: 'execute' },
|
|
1529
|
+
});
|
|
1530
|
+
// Match Start data ports to first node's data inputs
|
|
1531
|
+
if (firstNodeType) {
|
|
1532
|
+
for (const [portName, portDef] of Object.entries(startPorts)) {
|
|
1533
|
+
if (portDef.dataType === 'STEP')
|
|
1534
|
+
continue; // Skip control flow
|
|
1535
|
+
if (portName in firstNodeType.inputs && !isControlFlowPort(portName)) {
|
|
1536
|
+
connections.push({
|
|
1537
|
+
type: 'Connection',
|
|
1538
|
+
from: { node: 'Start', port: portName },
|
|
1539
|
+
to: { node: firstInstance.id, port: portName },
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
// Connect consecutive nodes
|
|
1546
|
+
for (let i = 0; i < instances.length - 1; i++) {
|
|
1547
|
+
const current = instances[i];
|
|
1548
|
+
const next = instances[i + 1];
|
|
1549
|
+
const currentNodeType = findNodeType(current.nodeType);
|
|
1550
|
+
const nextNodeType = findNodeType(next.nodeType);
|
|
1551
|
+
// current.onSuccess -> next.execute (execution flow)
|
|
1552
|
+
connections.push({
|
|
1553
|
+
type: 'Connection',
|
|
1554
|
+
from: { node: current.id, port: 'onSuccess' },
|
|
1555
|
+
to: { node: next.id, port: 'execute' },
|
|
1556
|
+
});
|
|
1557
|
+
// Match data ports: current outputs -> next inputs (by matching port names)
|
|
1558
|
+
if (currentNodeType && nextNodeType) {
|
|
1559
|
+
for (const [outputName, outputDef] of Object.entries(currentNodeType.outputs)) {
|
|
1560
|
+
if (outputDef.dataType === 'STEP' || isControlFlowPort(outputName))
|
|
1561
|
+
continue;
|
|
1562
|
+
if (outputName in nextNodeType.inputs && !isControlFlowPort(outputName)) {
|
|
1563
|
+
connections.push({
|
|
1564
|
+
type: 'Connection',
|
|
1565
|
+
from: { node: current.id, port: outputName },
|
|
1566
|
+
to: { node: next.id, port: outputName },
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
// Connect last node -> Exit
|
|
1573
|
+
if (instances.length > 0) {
|
|
1574
|
+
const lastInstance = instances[instances.length - 1];
|
|
1575
|
+
const lastNodeType = findNodeType(lastInstance.nodeType);
|
|
1576
|
+
// last.onSuccess -> Exit.onSuccess (execution flow)
|
|
1577
|
+
connections.push({
|
|
1578
|
+
type: 'Connection',
|
|
1579
|
+
from: { node: lastInstance.id, port: 'onSuccess' },
|
|
1580
|
+
to: { node: 'Exit', port: 'onSuccess' },
|
|
1581
|
+
});
|
|
1582
|
+
// Match last node's data outputs to Exit data ports
|
|
1583
|
+
if (lastNodeType) {
|
|
1584
|
+
for (const [portName, portDef] of Object.entries(exitPorts)) {
|
|
1585
|
+
if (portDef.dataType === 'STEP' || portDef.isControlFlow)
|
|
1586
|
+
continue;
|
|
1587
|
+
if (lastNodeType.outputs[portName] && !isControlFlowPort(portName)) {
|
|
1588
|
+
connections.push({
|
|
1589
|
+
type: 'Connection',
|
|
1590
|
+
from: { node: lastInstance.id, port: portName },
|
|
1591
|
+
to: { node: 'Exit', port: portName },
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return connections;
|
|
1598
|
+
}
|
|
1599
|
+
parseStartPorts(fn, config) {
|
|
1600
|
+
const ports = {};
|
|
1601
|
+
const params = fn.getParameters();
|
|
1602
|
+
// New architecture: first parameter should be execute: boolean
|
|
1603
|
+
// Second parameter is the params object with data
|
|
1604
|
+
if (params.length === 0) {
|
|
1605
|
+
// No parameters - just return execute port
|
|
1606
|
+
ports.execute = { dataType: 'STEP', label: 'Execute' };
|
|
1607
|
+
return ports;
|
|
1608
|
+
}
|
|
1609
|
+
// Check if first parameter is execute: boolean
|
|
1610
|
+
const firstParam = params[0];
|
|
1611
|
+
const firstParamName = firstParam.getName();
|
|
1612
|
+
const firstParamType = firstParam.getType();
|
|
1613
|
+
const firstParamTypeText = firstParamType.getText();
|
|
1614
|
+
if (firstParamName === 'execute' && firstParamTypeText === 'boolean') {
|
|
1615
|
+
// Correct new format: first param is execute
|
|
1616
|
+
// Check if JSDoc has explicit metadata override for execute port (from @param annotation)
|
|
1617
|
+
if (config?.startPorts && config.startPorts['execute']) {
|
|
1618
|
+
ports.execute = {
|
|
1619
|
+
dataType: 'STEP',
|
|
1620
|
+
tsType: 'boolean',
|
|
1621
|
+
...config.startPorts['execute'],
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
else {
|
|
1625
|
+
ports.execute = { dataType: 'STEP', tsType: 'boolean', label: 'Execute' };
|
|
1626
|
+
}
|
|
1627
|
+
// Extract data ports from second parameter if it exists
|
|
1628
|
+
if (params.length > 1) {
|
|
1629
|
+
const dataParam = params[1];
|
|
1630
|
+
const dataParamType = dataParam.getType();
|
|
1631
|
+
const properties = dataParamType.getProperties();
|
|
1632
|
+
properties.forEach((prop) => {
|
|
1633
|
+
const propName = prop.getName();
|
|
1634
|
+
const propType = prop.getTypeAtLocation(dataParam);
|
|
1635
|
+
const portType = this.inferPortType(propType);
|
|
1636
|
+
const propTypeText = propType.getText();
|
|
1637
|
+
// Extract schema for complex types (interfaces/objects)
|
|
1638
|
+
const tsSchema = portType === 'OBJECT' ? this.extractTypeSchema(propType) : undefined;
|
|
1639
|
+
// Check if JSDoc has explicit metadata override for this start port (from @param annotation)
|
|
1640
|
+
// Always prefer ts-morph inferred type over JSDoc regex-based inference
|
|
1641
|
+
// (JSDoc @param type inference uses regex which can fail for complex types)
|
|
1642
|
+
const startPortConfig = config?.startPorts?.[propName];
|
|
1643
|
+
ports[propName] = {
|
|
1644
|
+
dataType: portType,
|
|
1645
|
+
label: startPortConfig?.label || this.capitalize(propName),
|
|
1646
|
+
...(startPortConfig?.metadata && { metadata: startPortConfig.metadata }),
|
|
1647
|
+
// Include original TS type for rich type display
|
|
1648
|
+
...(propTypeText && { tsType: propTypeText }),
|
|
1649
|
+
// Include schema breakdown for complex types
|
|
1650
|
+
...(tsSchema && Object.keys(tsSchema).length > 0 && { tsSchema }),
|
|
1651
|
+
};
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
else {
|
|
1656
|
+
// Old format detected - reject it
|
|
1657
|
+
throw new Error(`Invalid node type function signature for "${fn.getName()}". ` +
|
|
1658
|
+
`Expected first parameter to be "execute: boolean", but got "${firstParamName}: ${firstParamTypeText}". ` +
|
|
1659
|
+
`Correct format: function ${fn.getName()}(execute: boolean, data: {...}) { ... }`);
|
|
1660
|
+
}
|
|
1661
|
+
// Assign implicit port orders with mandatory port precedence
|
|
1662
|
+
assignImplicitPortOrders(ports);
|
|
1663
|
+
return ports;
|
|
1664
|
+
}
|
|
1665
|
+
parseExitPorts(fn, config) {
|
|
1666
|
+
const ports = {};
|
|
1667
|
+
let returnType = fn.getReturnType();
|
|
1668
|
+
// If return type is a Promise, extract the type parameter
|
|
1669
|
+
const typeText = returnType?.getText();
|
|
1670
|
+
const sourceFile = fn.getSourceFile();
|
|
1671
|
+
const filePath = sourceFile?.getFilePath() || 'unknown file';
|
|
1672
|
+
const fileName = path.basename(filePath);
|
|
1673
|
+
if (!typeText || typeText === 'void') {
|
|
1674
|
+
console.warn(`[PARSER] Could not determine return type for function "${fn.getName()}" in ${fileName}.\n` +
|
|
1675
|
+
` Add an explicit return type like: Promise<{ onSuccess: boolean; onFailure: boolean }>`);
|
|
1676
|
+
return ports;
|
|
1677
|
+
}
|
|
1678
|
+
if (typeText.startsWith('Promise<')) {
|
|
1679
|
+
const typeArgs = returnType.getTypeArguments();
|
|
1680
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
1681
|
+
returnType = typeArgs[0];
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const properties = returnType.getProperties();
|
|
1685
|
+
properties.forEach((prop) => {
|
|
1686
|
+
const propName = prop.getName();
|
|
1687
|
+
// Always infer from TypeScript signature, use @returns annotation only for metadata
|
|
1688
|
+
// onSuccess/onFailure are always STEP ports regardless of annotations
|
|
1689
|
+
if (propName === 'onSuccess' || propName === 'onFailure') {
|
|
1690
|
+
const returnPortConfig = config?.returnPorts?.[propName];
|
|
1691
|
+
ports[propName] = {
|
|
1692
|
+
dataType: 'STEP',
|
|
1693
|
+
label: returnPortConfig?.label || (propName === 'onSuccess' ? 'On Success' : 'On Failure'),
|
|
1694
|
+
isControlFlow: true,
|
|
1695
|
+
...(returnPortConfig?.metadata && { metadata: returnPortConfig.metadata }),
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
// Auto-infer type from TypeScript signature (more accurate than JSDoc regex)
|
|
1700
|
+
const propType = prop.getTypeAtLocation(fn.getTypeResolutionNode());
|
|
1701
|
+
const portType = this.inferPortType(propType);
|
|
1702
|
+
const propTypeText = propType.getText();
|
|
1703
|
+
// Extract schema for complex types (interfaces/objects)
|
|
1704
|
+
const tsSchema = portType === 'OBJECT' ? this.extractTypeSchema(propType) : undefined;
|
|
1705
|
+
const returnPortConfig = config?.returnPorts?.[propName];
|
|
1706
|
+
ports[propName] = {
|
|
1707
|
+
dataType: portType,
|
|
1708
|
+
label: returnPortConfig?.label || this.capitalize(propName),
|
|
1709
|
+
...(returnPortConfig?.metadata && { metadata: returnPortConfig.metadata }),
|
|
1710
|
+
// Include original TS type for rich type display (e.g., "ResearchReport" instead of "object")
|
|
1711
|
+
...(propTypeText && propTypeText !== portType.toLowerCase() && { tsType: propTypeText }),
|
|
1712
|
+
// Include schema breakdown for complex types
|
|
1713
|
+
...(tsSchema && Object.keys(tsSchema).length > 0 && { tsSchema }),
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
// Assign implicit port orders with mandatory port precedence
|
|
1718
|
+
assignImplicitPortOrders(ports);
|
|
1719
|
+
return ports;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Extract schema breakdown for complex types (interfaces/objects).
|
|
1723
|
+
* Returns a map of property names to their TypeScript type strings.
|
|
1724
|
+
*/
|
|
1725
|
+
extractTypeSchema(tsType) {
|
|
1726
|
+
const schema = {};
|
|
1727
|
+
const properties = tsType.getProperties();
|
|
1728
|
+
if (!properties || properties.length === 0) {
|
|
1729
|
+
return undefined;
|
|
1730
|
+
}
|
|
1731
|
+
for (const prop of properties) {
|
|
1732
|
+
const propName = prop.getName();
|
|
1733
|
+
// Skip internal/private properties
|
|
1734
|
+
if (propName.startsWith('_'))
|
|
1735
|
+
continue;
|
|
1736
|
+
const propType = prop.getValueDeclaration()?.getType();
|
|
1737
|
+
if (propType) {
|
|
1738
|
+
schema[propName] = propType.getText();
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
// Fallback: try to get type from declarations
|
|
1742
|
+
const declarations = prop.getDeclarations();
|
|
1743
|
+
if (declarations && declarations.length > 0) {
|
|
1744
|
+
const decl = declarations[0];
|
|
1745
|
+
schema[propName] = decl.getType().getText();
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return Object.keys(schema).length > 0 ? schema : undefined;
|
|
1750
|
+
}
|
|
1751
|
+
inferPortType(tsType) {
|
|
1752
|
+
const typeText = tsType.getText();
|
|
1753
|
+
// Delegate to inferDataTypeFromTS for consistent type mapping
|
|
1754
|
+
// This handles all cases: primitives, any, unknown, never, arrays, functions, etc.
|
|
1755
|
+
return inferDataTypeFromTS(typeText);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Check if a function has a valid @flowWeaver annotation (nodeType, workflow, or pattern).
|
|
1759
|
+
* Avoids false positives from file-level JSDoc that mentions @flowWeaver in description text.
|
|
1760
|
+
*/
|
|
1761
|
+
hasFlowWeaverAnnotation(fn) {
|
|
1762
|
+
const validTypes = new Set(['nodeType', 'workflow', 'pattern']);
|
|
1763
|
+
return fn.getJsDocs().some((doc) => doc.getTags().some((t) => {
|
|
1764
|
+
if (t.getTagName() !== 'flowWeaver')
|
|
1765
|
+
return false;
|
|
1766
|
+
const comment = t.getCommentText?.()?.trim() || '';
|
|
1767
|
+
return validTypes.has(comment.split(/\s/)[0]);
|
|
1768
|
+
}));
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Generate an annotation suggestion for ghost-text autocomplete.
|
|
1772
|
+
* Analyzes the function nearest to cursorLine, diffs inferred ports against
|
|
1773
|
+
* any existing @flowWeaver annotation, and returns only the missing lines.
|
|
1774
|
+
*/
|
|
1775
|
+
generateAnnotationSuggestion(content, cursorLine, virtualPath = 'virtual.ts') {
|
|
1776
|
+
// Create virtual SourceFile
|
|
1777
|
+
const existingFile = this.project.getSourceFile(virtualPath);
|
|
1778
|
+
if (existingFile) {
|
|
1779
|
+
this.project.removeSourceFile(existingFile);
|
|
1780
|
+
}
|
|
1781
|
+
const sourceFile = this.project.createSourceFile(virtualPath, content, { overwrite: true });
|
|
1782
|
+
try {
|
|
1783
|
+
const allFunctions = extractFunctionLikes(sourceFile);
|
|
1784
|
+
if (allFunctions.length === 0)
|
|
1785
|
+
return null;
|
|
1786
|
+
// Find the function nearest to cursorLine (below or containing the cursor)
|
|
1787
|
+
// cursorLine is 0-based; getStartLineNumber() is 1-based
|
|
1788
|
+
let targetFn = null;
|
|
1789
|
+
let bestDistance = Infinity;
|
|
1790
|
+
for (const fn of allFunctions) {
|
|
1791
|
+
const fnLine = fn.getStartLineNumber(false) - 1; // 0-based
|
|
1792
|
+
// Prefer functions at or below the cursor
|
|
1793
|
+
const distance = fnLine >= cursorLine ? fnLine - cursorLine : (cursorLine - fnLine) + 1000;
|
|
1794
|
+
if (distance < bestDistance) {
|
|
1795
|
+
bestDistance = distance;
|
|
1796
|
+
targetFn = fn;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (!targetFn)
|
|
1800
|
+
return null;
|
|
1801
|
+
const fnName = targetFn.getName() || 'anonymous';
|
|
1802
|
+
const fnStartLine = targetFn.getStartLineNumber(false) - 1; // 0-based
|
|
1803
|
+
// Don't suggest if cursor is too far from the function (more than 30 lines above)
|
|
1804
|
+
if (cursorLine < fnStartLine - 30)
|
|
1805
|
+
return null;
|
|
1806
|
+
// Check existing JSDoc state
|
|
1807
|
+
const hasAnnotation = this.hasFlowWeaverAnnotation(targetFn);
|
|
1808
|
+
const hasAnyJsDoc = targetFn.getJsDocs().length > 0;
|
|
1809
|
+
// If function has a JSDoc but NOT a @flowWeaver annotation, don't suggest
|
|
1810
|
+
// a competing JSDoc block — the user has an intentional regular JSDoc
|
|
1811
|
+
if (hasAnyJsDoc && !hasAnnotation)
|
|
1812
|
+
return null;
|
|
1813
|
+
// Infer full node type from function signature
|
|
1814
|
+
const inferred = this.inferNodeTypeFromFunction(targetFn, fnName, virtualPath);
|
|
1815
|
+
// Extract @param descriptions from existing JSDoc (if any)
|
|
1816
|
+
const paramDescriptions = new Map();
|
|
1817
|
+
for (const doc of targetFn.getJsDocs()) {
|
|
1818
|
+
for (const tag of doc.getTags()) {
|
|
1819
|
+
if (tag.getTagName() === 'param') {
|
|
1820
|
+
const comment = tag.getCommentText?.()?.trim() || '';
|
|
1821
|
+
// Extract param name and description: "{type} name - desc" or "name - desc" or "name desc"
|
|
1822
|
+
const paramMatch = comment.match(/^(?:\{[^}]*\}\s+)?(\w+)(?:\s*-\s*|\s+)(.+)/);
|
|
1823
|
+
if (paramMatch) {
|
|
1824
|
+
paramDescriptions.set(paramMatch[1], paramMatch[2]);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Merge @param descriptions into inferred port labels
|
|
1830
|
+
for (const [portName, portDef] of Object.entries(inferred.inputs)) {
|
|
1831
|
+
const desc = paramDescriptions.get(portName);
|
|
1832
|
+
if (desc) {
|
|
1833
|
+
portDef.label = desc;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
// Parse existing JSDoc to find what's already annotated
|
|
1837
|
+
const existingPorts = this.extractExistingAnnotatedPorts(targetFn);
|
|
1838
|
+
// Build missing port lines
|
|
1839
|
+
const missingLines = [];
|
|
1840
|
+
// Filter out mandatory ports (execute, onSuccess, onFailure) from suggestions
|
|
1841
|
+
for (const [portName, portDef] of Object.entries(inferred.inputs)) {
|
|
1842
|
+
if (isControlFlowPort(portName))
|
|
1843
|
+
continue;
|
|
1844
|
+
if (existingPorts.inputs.has(portName))
|
|
1845
|
+
continue;
|
|
1846
|
+
missingLines.push(` * ${generateJSDocPortTag(portName, portDef, 'input')}`);
|
|
1847
|
+
}
|
|
1848
|
+
for (const [portName, portDef] of Object.entries(inferred.outputs)) {
|
|
1849
|
+
if (isControlFlowPort(portName))
|
|
1850
|
+
continue;
|
|
1851
|
+
if (existingPorts.outputs.has(portName))
|
|
1852
|
+
continue;
|
|
1853
|
+
missingLines.push(` * ${generateJSDocPortTag(portName, portDef, 'output')}`);
|
|
1854
|
+
}
|
|
1855
|
+
if (hasAnnotation) {
|
|
1856
|
+
// Check if this is a workflow block — if so, suggest missing connections
|
|
1857
|
+
const isWorkflow = this.isWorkflowBlock(targetFn);
|
|
1858
|
+
if (isWorkflow) {
|
|
1859
|
+
const connectionLines = this.generateWorkflowStructureSuggestion(targetFn, sourceFile);
|
|
1860
|
+
missingLines.push(...connectionLines);
|
|
1861
|
+
}
|
|
1862
|
+
// Partial JSDoc: suggest only missing ports / connections
|
|
1863
|
+
if (missingLines.length === 0)
|
|
1864
|
+
return null;
|
|
1865
|
+
// Find the insertion point: just before the closing */
|
|
1866
|
+
const lines = content.split('\n');
|
|
1867
|
+
let jsDocEndLine = -1;
|
|
1868
|
+
for (let i = fnStartLine - 1; i >= 0; i--) {
|
|
1869
|
+
if (lines[i].includes('*/')) {
|
|
1870
|
+
jsDocEndLine = i;
|
|
1871
|
+
break;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (jsDocEndLine < 0)
|
|
1875
|
+
return null;
|
|
1876
|
+
const text = missingLines.join('\n') + '\n';
|
|
1877
|
+
return {
|
|
1878
|
+
text,
|
|
1879
|
+
insertLine: jsDocEndLine,
|
|
1880
|
+
replaceLinesCount: 0,
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
// No @flowWeaver JSDoc — check if user just typed "/**" on the cursor line
|
|
1884
|
+
const lines = content.split('\n');
|
|
1885
|
+
const cursorLineText = lines[cursorLine] || '';
|
|
1886
|
+
if (/^\s*\/\*\*\s*$/.test(cursorLineText)) {
|
|
1887
|
+
// User typed "/**" — generate only the continuation lines after it
|
|
1888
|
+
const continuationLines = [
|
|
1889
|
+
` * @flowWeaver nodeType ${fnName}`,
|
|
1890
|
+
...(inferred.expression ? [' * @expression'] : []),
|
|
1891
|
+
...missingLines,
|
|
1892
|
+
' */',
|
|
1893
|
+
];
|
|
1894
|
+
const text = continuationLines.join('\n') + '\n';
|
|
1895
|
+
return {
|
|
1896
|
+
text,
|
|
1897
|
+
insertLine: cursorLine + 1,
|
|
1898
|
+
replaceLinesCount: 0,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
// Generate full annotation block
|
|
1902
|
+
const allLines = [
|
|
1903
|
+
'/**',
|
|
1904
|
+
` * @flowWeaver nodeType ${fnName}`,
|
|
1905
|
+
...(inferred.expression ? [' * @expression'] : []),
|
|
1906
|
+
...missingLines,
|
|
1907
|
+
' */',
|
|
1908
|
+
];
|
|
1909
|
+
const text = allLines.join('\n') + '\n';
|
|
1910
|
+
// Insert on the line above the function
|
|
1911
|
+
return {
|
|
1912
|
+
text,
|
|
1913
|
+
insertLine: fnStartLine,
|
|
1914
|
+
replaceLinesCount: 0,
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
finally {
|
|
1918
|
+
// Clean up virtual source file
|
|
1919
|
+
const sf = this.project.getSourceFile(virtualPath);
|
|
1920
|
+
if (sf)
|
|
1921
|
+
this.project.removeSourceFile(sf);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Check if a function's JSDoc marks it as a workflow (vs nodeType).
|
|
1926
|
+
*/
|
|
1927
|
+
isWorkflowBlock(fn) {
|
|
1928
|
+
for (const doc of fn.getJsDocs()) {
|
|
1929
|
+
for (const tag of doc.getTags()) {
|
|
1930
|
+
if (tag.getTagName() !== 'flowWeaver')
|
|
1931
|
+
continue;
|
|
1932
|
+
const comment = tag.getCommentText?.()?.trim() || '';
|
|
1933
|
+
const firstWord = comment.split(/\s/)[0];
|
|
1934
|
+
// 'workflow' explicitly, or bare @flowWeaver (no qualifier), or named workflow
|
|
1935
|
+
if (firstWord === 'workflow' || firstWord === '' || (firstWord !== 'nodeType' && firstWord !== 'pattern')) {
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Generate missing @connect suggestions for a workflow block.
|
|
1944
|
+
* Finds @node declarations, resolves their types, and suggests connections
|
|
1945
|
+
* for matching port names that aren't already wired.
|
|
1946
|
+
*/
|
|
1947
|
+
generateWorkflowStructureSuggestion(fn, sourceFile) {
|
|
1948
|
+
// Extract @node declarations: { nodeId -> nodeTypeName }
|
|
1949
|
+
const nodeDecls = new Map();
|
|
1950
|
+
// Extract existing @connect lines: set of "sourceNode.sourcePort->targetNode.targetPort"
|
|
1951
|
+
const existingConnections = new Set();
|
|
1952
|
+
for (const doc of fn.getJsDocs()) {
|
|
1953
|
+
for (const tag of doc.getTags()) {
|
|
1954
|
+
const tagName = tag.getTagName();
|
|
1955
|
+
const comment = tag.getCommentText?.()?.trim() || '';
|
|
1956
|
+
if (tagName === 'node') {
|
|
1957
|
+
const nodeMatch = comment.match(/^(\w+)\s+(\w+)/);
|
|
1958
|
+
if (nodeMatch) {
|
|
1959
|
+
nodeDecls.set(nodeMatch[1], nodeMatch[2]);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
else if (tagName === 'connect') {
|
|
1963
|
+
const connMatch = comment.match(/^(\w+)\.(\w+)\s*->\s*(\w+)\.(\w+)/);
|
|
1964
|
+
if (connMatch) {
|
|
1965
|
+
existingConnections.add(`${connMatch[1]}.${connMatch[2]}->${connMatch[3]}.${connMatch[4]}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (nodeDecls.size < 2)
|
|
1971
|
+
return [];
|
|
1972
|
+
// Resolve node types from the same file
|
|
1973
|
+
const allFunctions = extractFunctionLikes(sourceFile);
|
|
1974
|
+
const resolvedTypes = new Map();
|
|
1975
|
+
for (const [nodeId, typeName] of nodeDecls) {
|
|
1976
|
+
const matchedFn = allFunctions.find((f) => f.getName() === typeName);
|
|
1977
|
+
if (matchedFn) {
|
|
1978
|
+
resolvedTypes.set(nodeId, this.inferNodeTypeFromFunction(matchedFn, typeName, sourceFile.getFilePath()));
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
// Find matching unconnected port pairs
|
|
1982
|
+
const suggestions = [];
|
|
1983
|
+
const nodeIds = [...nodeDecls.keys()];
|
|
1984
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
1985
|
+
for (let j = 0; j < nodeIds.length; j++) {
|
|
1986
|
+
if (i === j)
|
|
1987
|
+
continue;
|
|
1988
|
+
const srcId = nodeIds[i];
|
|
1989
|
+
const tgtId = nodeIds[j];
|
|
1990
|
+
const srcType = resolvedTypes.get(srcId);
|
|
1991
|
+
const tgtType = resolvedTypes.get(tgtId);
|
|
1992
|
+
if (!srcType || !tgtType)
|
|
1993
|
+
continue;
|
|
1994
|
+
for (const [outputName, outputDef] of Object.entries(srcType.outputs)) {
|
|
1995
|
+
if (isControlFlowPort(outputName))
|
|
1996
|
+
continue;
|
|
1997
|
+
if (outputDef.dataType === 'STEP')
|
|
1998
|
+
continue;
|
|
1999
|
+
// Check if target has a matching input with the same name
|
|
2000
|
+
if (outputName in tgtType.inputs && !isControlFlowPort(outputName)) {
|
|
2001
|
+
const connKey = `${srcId}.${outputName}->${tgtId}.${outputName}`;
|
|
2002
|
+
if (!existingConnections.has(connKey)) {
|
|
2003
|
+
suggestions.push(` * @connect ${srcId}.${outputName} -> ${tgtId}.${outputName}`);
|
|
2004
|
+
existingConnections.add(connKey); // prevent duplicates
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return suggestions;
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Extract port names that are already annotated in a function's JSDoc.
|
|
2014
|
+
* Returns sets of input and output port names found in existing annotations.
|
|
2015
|
+
*/
|
|
2016
|
+
extractExistingAnnotatedPorts(fn) {
|
|
2017
|
+
const inputs = new Set();
|
|
2018
|
+
const outputs = new Set();
|
|
2019
|
+
for (const doc of fn.getJsDocs()) {
|
|
2020
|
+
for (const tag of doc.getTags()) {
|
|
2021
|
+
const tagName = tag.getTagName();
|
|
2022
|
+
const comment = tag.getCommentText?.()?.trim() || '';
|
|
2023
|
+
// Extract port name: first word, possibly wrapped in brackets [name] or [name=default]
|
|
2024
|
+
const nameMatch = comment.match(/^\[?(\w+)/);
|
|
2025
|
+
if (!nameMatch)
|
|
2026
|
+
continue;
|
|
2027
|
+
const portName = nameMatch[1];
|
|
2028
|
+
if (tagName === 'input' || tagName === 'step') {
|
|
2029
|
+
inputs.add(portName);
|
|
2030
|
+
}
|
|
2031
|
+
else if (tagName === 'output') {
|
|
2032
|
+
outputs.add(portName);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
return { inputs, outputs };
|
|
2037
|
+
}
|
|
2038
|
+
capitalize(str) {
|
|
2039
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
export const parser = new AnnotationParser();
|
|
2043
|
+
/**
|
|
2044
|
+
* Resolve npm node types by re-reading their .d.ts files.
|
|
2045
|
+
* This fills in the full port information that isn't stored in @fwImport annotations.
|
|
2046
|
+
*
|
|
2047
|
+
* When workflows are parsed, npm node types from @fwImport annotations only contain
|
|
2048
|
+
* minimal stub information (name, functionName, importSource). This function re-resolves
|
|
2049
|
+
* the full port definitions from the actual .d.ts files of the npm packages.
|
|
2050
|
+
*
|
|
2051
|
+
* @param ast - The workflow AST with potentially stub npm node types
|
|
2052
|
+
* @param workdir - Directory to search for node_modules (typically the workflow file's directory)
|
|
2053
|
+
* @returns Updated AST with fully resolved npm node types
|
|
2054
|
+
*/
|
|
2055
|
+
export function resolveNpmNodeTypes(ast, workdir) {
|
|
2056
|
+
if (!ast.nodeTypes || ast.nodeTypes.length === 0) {
|
|
2057
|
+
return ast;
|
|
2058
|
+
}
|
|
2059
|
+
const resolvedNodeTypes = ast.nodeTypes.map((nodeType) => {
|
|
2060
|
+
// Only resolve npm node types (those with importSource)
|
|
2061
|
+
if (!nodeType.importSource) {
|
|
2062
|
+
return nodeType;
|
|
2063
|
+
}
|
|
2064
|
+
// Get the full node type from the .d.ts file
|
|
2065
|
+
const packageExports = getPackageExports(nodeType.importSource, workdir);
|
|
2066
|
+
const matchingExport = packageExports.find((exp) => exp.name === nodeType.name || exp.function === nodeType.functionName);
|
|
2067
|
+
if (!matchingExport) {
|
|
2068
|
+
// Can't resolve - keep stub (will show only result port)
|
|
2069
|
+
return nodeType;
|
|
2070
|
+
}
|
|
2071
|
+
// Convert TNpmNodeType ports to TNodeTypeAST inputs/outputs
|
|
2072
|
+
const inputs = {};
|
|
2073
|
+
const outputs = {};
|
|
2074
|
+
for (const port of matchingExport.ports) {
|
|
2075
|
+
if (port.direction === 'INPUT') {
|
|
2076
|
+
inputs[port.name] = {
|
|
2077
|
+
dataType: port.type,
|
|
2078
|
+
label: port.defaultLabel,
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
else if (port.direction === 'OUTPUT') {
|
|
2082
|
+
outputs[port.name] = {
|
|
2083
|
+
dataType: port.type,
|
|
2084
|
+
label: port.defaultLabel,
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return {
|
|
2089
|
+
...nodeType,
|
|
2090
|
+
inputs,
|
|
2091
|
+
outputs,
|
|
2092
|
+
isAsync: matchingExport.synchronicity === 'ASYNC',
|
|
2093
|
+
};
|
|
2094
|
+
});
|
|
2095
|
+
return {
|
|
2096
|
+
...ast,
|
|
2097
|
+
nodeTypes: resolvedNodeTypes,
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
//# sourceMappingURL=parser.js.map
|