@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
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
import { RESERVED_NODE_NAMES, isStartNode, isExitNode, isExecutePort, isReservedNodeName, } from './constants.js';
|
|
2
|
+
import { findClosestMatches } from './utils/string-distance.js';
|
|
3
|
+
import { parseFunctionSignature } from './jsdoc-port-sync/signature-parser.js';
|
|
4
|
+
export class WorkflowValidator {
|
|
5
|
+
errors = [];
|
|
6
|
+
warnings = [];
|
|
7
|
+
strictMode = false;
|
|
8
|
+
/** Look up instance sourceLocation by instance ID */
|
|
9
|
+
getInstanceLocation(workflow, instanceId) {
|
|
10
|
+
const instance = workflow.instances.find((inst) => inst.id === instanceId);
|
|
11
|
+
return instance?.sourceLocation;
|
|
12
|
+
}
|
|
13
|
+
/** Look up connection sourceLocation */
|
|
14
|
+
getConnectionLocation(conn) {
|
|
15
|
+
return conn.sourceLocation;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validate a single node type for scoped port requirements
|
|
19
|
+
*
|
|
20
|
+
* Scoped Port Architecture Rules:
|
|
21
|
+
* - Scope names must be valid JavaScript identifiers
|
|
22
|
+
*
|
|
23
|
+
* Per-Port Scope Architecture:
|
|
24
|
+
* - Scoped OUTPUT ports become callback PARAMETERS (data flows to children)
|
|
25
|
+
* - Scoped INPUT ports become callback RETURN VALUES (data flows from children)
|
|
26
|
+
* - Scoped ports can be ANY data type - they're not functions themselves
|
|
27
|
+
* - The callback function is passed as a function parameter (e.g., forEach's itemProcessor)
|
|
28
|
+
*
|
|
29
|
+
* NOTE: execute/onSuccess/onFailure ports are mandatory base interface ports
|
|
30
|
+
* that are auto-added to ALL nodes - no validation needed for those.
|
|
31
|
+
*/
|
|
32
|
+
validateNodeType(nodeType) {
|
|
33
|
+
const errors = [];
|
|
34
|
+
// Get all scoped ports (both INPUT and OUTPUT)
|
|
35
|
+
const scopedPorts = [
|
|
36
|
+
...Object.entries(nodeType.inputs).filter(([_, portDef]) => portDef.scope !== undefined),
|
|
37
|
+
...Object.entries(nodeType.outputs).filter(([_, portDef]) => portDef.scope !== undefined),
|
|
38
|
+
];
|
|
39
|
+
// Rule: Validate scope names are valid JavaScript identifiers
|
|
40
|
+
const scopeNameRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
41
|
+
scopedPorts.forEach(([portName, portDef]) => {
|
|
42
|
+
if (portDef.scope && !scopeNameRegex.test(portDef.scope)) {
|
|
43
|
+
errors.push(`Port "${portName}" has invalid scope name "${portDef.scope}". Scope names must be valid JavaScript identifiers (letters, numbers, underscore, dollar sign; cannot start with number).`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Note: Scoped ports can be ANY data type in the per-port scope architecture
|
|
47
|
+
// They become callback parameters/returns, not functions themselves
|
|
48
|
+
return errors;
|
|
49
|
+
}
|
|
50
|
+
validate(workflow, options) {
|
|
51
|
+
this.errors = [];
|
|
52
|
+
this.warnings = [];
|
|
53
|
+
this.strictMode = options?.strictMode ?? false;
|
|
54
|
+
const nodeTypeMap = new Map();
|
|
55
|
+
// Map by both functionName and name to support npm nodes (name='npm/pkg/func', functionName='func')
|
|
56
|
+
workflow.nodeTypes.forEach((nodeType) => {
|
|
57
|
+
nodeTypeMap.set(nodeType.functionName, nodeType);
|
|
58
|
+
if (nodeType.name !== nodeType.functionName) {
|
|
59
|
+
nodeTypeMap.set(nodeType.name, nodeType);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Build instance map: instance ID -> node type
|
|
63
|
+
const instanceMap = new Map();
|
|
64
|
+
workflow.instances.forEach((instance) => {
|
|
65
|
+
// Check both name (for npm nodes like 'npm/pkg/func') and functionName (for local nodes)
|
|
66
|
+
const nodeType = workflow.nodeTypes.find((nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType);
|
|
67
|
+
if (nodeType) {
|
|
68
|
+
instanceMap.set(instance.id, nodeType);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Check if the function exists but is unannotated
|
|
72
|
+
const isUnannotatedFunction = workflow.availableFunctionNames?.includes(instance.nodeType) ?? false;
|
|
73
|
+
let hint;
|
|
74
|
+
if (isUnannotatedFunction) {
|
|
75
|
+
hint = ` Function "${instance.nodeType}" exists but has no @flowWeaver nodeType annotation. Add /** @flowWeaver nodeType */ above it.`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const availableTypes = workflow.nodeTypes.map((nt) => nt.functionName);
|
|
79
|
+
const suggestions = findClosestMatches(instance.nodeType, availableTypes);
|
|
80
|
+
hint = suggestions.length > 0 ? ` Did you mean "${suggestions[0]}"?` : '';
|
|
81
|
+
}
|
|
82
|
+
this.errors.push({
|
|
83
|
+
type: 'error',
|
|
84
|
+
code: 'UNKNOWN_NODE_TYPE',
|
|
85
|
+
message: `Node "${instance.id}" references unknown node type "${instance.nodeType}".${hint}`,
|
|
86
|
+
node: instance.id,
|
|
87
|
+
location: instance.sourceLocation,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Info diagnostic for auto-inferred node types
|
|
92
|
+
workflow.instances.forEach((instance) => {
|
|
93
|
+
// Check both name (for npm nodes like 'npm/pkg/func') and functionName (for local nodes)
|
|
94
|
+
const nodeType = workflow.nodeTypes.find((nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType);
|
|
95
|
+
if (nodeType?.inferred) {
|
|
96
|
+
this.warnings.push({
|
|
97
|
+
type: 'warning',
|
|
98
|
+
code: 'INFERRED_NODE_TYPE',
|
|
99
|
+
message: `Node type "${instance.nodeType}" was auto-inferred from function signature (expression mode). Add @flowWeaver nodeType for explicit port control.`,
|
|
100
|
+
node: instance.id,
|
|
101
|
+
location: instance.sourceLocation,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Structural validation
|
|
106
|
+
this.validateStructure(workflow);
|
|
107
|
+
this.validateDuplicateNodeNames(workflow);
|
|
108
|
+
this.validateMutableBindings(workflow);
|
|
109
|
+
// Connection and node validation
|
|
110
|
+
this.validateReservedNames(workflow, nodeTypeMap);
|
|
111
|
+
this.validateConnections(workflow, instanceMap);
|
|
112
|
+
this.validateNodeReferences(workflow, instanceMap);
|
|
113
|
+
this.validateTypeCompatibility(workflow, instanceMap);
|
|
114
|
+
this.validateRequiredInputs(workflow, instanceMap);
|
|
115
|
+
this.detectUnusedNodes(workflow, instanceMap);
|
|
116
|
+
this.validateStartAndExit(workflow);
|
|
117
|
+
this.validateDataFlow(workflow, instanceMap);
|
|
118
|
+
this.validateCycles(workflow);
|
|
119
|
+
this.validateMultipleInputConnections(workflow, instanceMap);
|
|
120
|
+
this.validateAnnotationSignatureConsistency(workflow);
|
|
121
|
+
// Deduplicate cascading errors: if a node has UNKNOWN_NODE_TYPE,
|
|
122
|
+
// suppress UNKNOWN_SOURCE_NODE, UNKNOWN_TARGET_NODE, and UNDEFINED_NODE
|
|
123
|
+
// that reference the same node IDs (they're just noise).
|
|
124
|
+
const unknownTypeInstanceIds = new Set(workflow.instances.filter((inst) => !nodeTypeMap.has(inst.nodeType)).map((inst) => inst.id));
|
|
125
|
+
if (unknownTypeInstanceIds.size > 0) {
|
|
126
|
+
this.errors = this.errors.filter((error) => {
|
|
127
|
+
if (error.code === 'UNKNOWN_NODE_TYPE')
|
|
128
|
+
return true; // Always keep root cause
|
|
129
|
+
// Suppress cascading errors that reference unknown-type instances
|
|
130
|
+
const cascadingCodes = new Set([
|
|
131
|
+
'UNKNOWN_SOURCE_NODE',
|
|
132
|
+
'UNKNOWN_TARGET_NODE',
|
|
133
|
+
'UNDEFINED_NODE',
|
|
134
|
+
'MISSING_REQUIRED_INPUT',
|
|
135
|
+
]);
|
|
136
|
+
if (!cascadingCodes.has(error.code))
|
|
137
|
+
return true;
|
|
138
|
+
// Check if this error references an unknown-type instance
|
|
139
|
+
if (error.node && unknownTypeInstanceIds.has(error.node))
|
|
140
|
+
return false;
|
|
141
|
+
if (error.connection) {
|
|
142
|
+
if (unknownTypeInstanceIds.has(error.connection.from.node))
|
|
143
|
+
return false;
|
|
144
|
+
if (unknownTypeInstanceIds.has(error.connection.to.node))
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
valid: this.errors.length === 0,
|
|
152
|
+
errors: this.errors,
|
|
153
|
+
warnings: this.warnings,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
validateStructure(workflow) {
|
|
157
|
+
if (!workflow.name) {
|
|
158
|
+
this.errors.push({
|
|
159
|
+
type: 'error',
|
|
160
|
+
code: 'MISSING_WORKFLOW_NAME',
|
|
161
|
+
message: 'Workflow must have a name',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (!workflow.functionName) {
|
|
165
|
+
this.errors.push({
|
|
166
|
+
type: 'error',
|
|
167
|
+
code: 'MISSING_FUNCTION_NAME',
|
|
168
|
+
message: 'Workflow must have a functionName',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
validateDuplicateNodeNames(workflow) {
|
|
173
|
+
const nodeNames = new Set();
|
|
174
|
+
workflow.nodeTypes.forEach((nodeType) => {
|
|
175
|
+
if (nodeNames.has(nodeType.functionName)) {
|
|
176
|
+
this.errors.push({
|
|
177
|
+
type: 'error',
|
|
178
|
+
code: 'DUPLICATE_NODE_NAME',
|
|
179
|
+
message: `Duplicate node type name: "${nodeType.functionName}"`,
|
|
180
|
+
node: nodeType.functionName,
|
|
181
|
+
location: nodeType.sourceLocation,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
nodeNames.add(nodeType.functionName);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
validateMutableBindings(workflow) {
|
|
188
|
+
workflow.nodeTypes.forEach((nodeType) => {
|
|
189
|
+
if (nodeType.declarationKind && nodeType.declarationKind !== 'const') {
|
|
190
|
+
this.warnings.push({
|
|
191
|
+
type: 'warning',
|
|
192
|
+
code: 'MUTABLE_NODE_TYPE_BINDING',
|
|
193
|
+
message: `Node type "${nodeType.functionName}" is declared with "${nodeType.declarationKind}" instead of "const". Use "const" to prevent accidental reassignment.`,
|
|
194
|
+
node: nodeType.functionName,
|
|
195
|
+
location: nodeType.sourceLocation,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
validateReservedNames(workflow, nodeTypeMap) {
|
|
201
|
+
// Check for node types with reserved names (Start, Exit)
|
|
202
|
+
// Note: Port name validation is done during parsing
|
|
203
|
+
nodeTypeMap.forEach((nodeType, nodeName) => {
|
|
204
|
+
if (isReservedNodeName(nodeName)) {
|
|
205
|
+
this.errors.push({
|
|
206
|
+
type: 'error',
|
|
207
|
+
code: 'RESERVED_NODE_NAME',
|
|
208
|
+
message: `Node type name "${nodeName}" is reserved. Reserved node names: ${Object.values(RESERVED_NODE_NAMES).join(', ')}`,
|
|
209
|
+
node: nodeName,
|
|
210
|
+
location: nodeType.sourceLocation,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// Check for instances with reserved IDs
|
|
215
|
+
workflow.instances.forEach((instance) => {
|
|
216
|
+
if (isReservedNodeName(instance.id)) {
|
|
217
|
+
this.errors.push({
|
|
218
|
+
type: 'error',
|
|
219
|
+
code: 'RESERVED_INSTANCE_ID',
|
|
220
|
+
message: `Instance ID "${instance.id}" is reserved. Reserved names: ${Object.values(RESERVED_NODE_NAMES).join(', ')}`,
|
|
221
|
+
node: instance.id,
|
|
222
|
+
location: instance.sourceLocation,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
validateConnections(workflow, instanceMap) {
|
|
228
|
+
workflow.connections.forEach((conn, _index) => {
|
|
229
|
+
const fromNode = conn.from.node;
|
|
230
|
+
const fromPort = conn.from.port;
|
|
231
|
+
const connLocation = this.getConnectionLocation(conn);
|
|
232
|
+
if (!isStartNode(fromNode) && !instanceMap.has(fromNode)) {
|
|
233
|
+
const instanceIds = [...instanceMap.keys()];
|
|
234
|
+
const suggestions = findClosestMatches(fromNode, instanceIds);
|
|
235
|
+
const suggestion = suggestions.length > 0 ? ` Did you mean "${suggestions[0]}"?` : '';
|
|
236
|
+
this.errors.push({
|
|
237
|
+
type: 'error',
|
|
238
|
+
code: 'UNKNOWN_SOURCE_NODE',
|
|
239
|
+
message: `Connection references unknown source node: "${fromNode}"${suggestion}`,
|
|
240
|
+
connection: conn,
|
|
241
|
+
location: connLocation,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const toNode = conn.to.node;
|
|
245
|
+
const toPort = conn.to.port;
|
|
246
|
+
if (!isExitNode(toNode) && !instanceMap.has(toNode)) {
|
|
247
|
+
const instanceIds = [...instanceMap.keys()];
|
|
248
|
+
const suggestions = findClosestMatches(toNode, instanceIds);
|
|
249
|
+
const suggestion = suggestions.length > 0 ? ` Did you mean "${suggestions[0]}"?` : '';
|
|
250
|
+
this.errors.push({
|
|
251
|
+
type: 'error',
|
|
252
|
+
code: 'UNKNOWN_TARGET_NODE',
|
|
253
|
+
message: `Connection references unknown target node: "${toNode}"${suggestion}`,
|
|
254
|
+
connection: conn,
|
|
255
|
+
location: connLocation,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (!isStartNode(fromNode)) {
|
|
259
|
+
const sourceNode = instanceMap.get(fromNode);
|
|
260
|
+
if (sourceNode && !sourceNode.outputs.hasOwnProperty(fromPort)) {
|
|
261
|
+
const portNames = Object.keys(sourceNode.outputs);
|
|
262
|
+
const suggestions = findClosestMatches(fromPort, portNames);
|
|
263
|
+
const suggestion = suggestions.length > 0 ? ` Did you mean "${suggestions[0]}"?` : '';
|
|
264
|
+
this.errors.push({
|
|
265
|
+
type: 'error',
|
|
266
|
+
code: 'UNKNOWN_SOURCE_PORT',
|
|
267
|
+
message: `Node "${fromNode}" does not have output port "${fromPort}"${suggestion}`,
|
|
268
|
+
node: fromNode,
|
|
269
|
+
connection: conn,
|
|
270
|
+
location: connLocation,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Validate Start output port against declared @param ports
|
|
276
|
+
// 'execute' is always implicit on Start
|
|
277
|
+
const validStartPorts = new Set(['execute', ...Object.keys(workflow.startPorts)]);
|
|
278
|
+
if (!validStartPorts.has(fromPort)) {
|
|
279
|
+
const portNames = Array.from(validStartPorts);
|
|
280
|
+
const suggestions = findClosestMatches(fromPort, portNames);
|
|
281
|
+
let hint;
|
|
282
|
+
if (suggestions.length > 0) {
|
|
283
|
+
hint = ` Did you mean "${suggestions[0]}"?`;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
hint = `\nAdd '@param ${fromPort}' to the workflow JSDoc and include it in the params object:\n(execute: boolean, params: { ${fromPort}: type, ... })`;
|
|
287
|
+
}
|
|
288
|
+
this.errors.push({
|
|
289
|
+
type: 'error',
|
|
290
|
+
code: 'UNKNOWN_SOURCE_PORT',
|
|
291
|
+
message: `Start node does not have output port "${fromPort}".${hint}`,
|
|
292
|
+
node: fromNode,
|
|
293
|
+
connection: conn,
|
|
294
|
+
location: connLocation,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!isExitNode(toNode)) {
|
|
299
|
+
const targetNode = instanceMap.get(toNode);
|
|
300
|
+
if (targetNode && !targetNode.inputs.hasOwnProperty(toPort)) {
|
|
301
|
+
const portNames = Object.keys(targetNode.inputs);
|
|
302
|
+
const suggestions = findClosestMatches(toPort, portNames);
|
|
303
|
+
const suggestion = suggestions.length > 0 ? ` Did you mean "${suggestions[0]}"?` : '';
|
|
304
|
+
this.errors.push({
|
|
305
|
+
type: 'error',
|
|
306
|
+
code: 'UNKNOWN_TARGET_PORT',
|
|
307
|
+
message: `Node "${toNode}" does not have input port "${toPort}"${suggestion}`,
|
|
308
|
+
node: toNode,
|
|
309
|
+
connection: conn,
|
|
310
|
+
location: connLocation,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Validate Exit input port against declared @returns ports
|
|
316
|
+
// 'onSuccess' and 'onFailure' are always implicit on Exit
|
|
317
|
+
const validExitPorts = new Set([
|
|
318
|
+
'onSuccess',
|
|
319
|
+
'onFailure',
|
|
320
|
+
...Object.keys(workflow.exitPorts),
|
|
321
|
+
]);
|
|
322
|
+
if (!validExitPorts.has(toPort)) {
|
|
323
|
+
const portNames = Array.from(validExitPorts);
|
|
324
|
+
const suggestions = findClosestMatches(toPort, portNames);
|
|
325
|
+
const suggestion = suggestions.length > 0 ? ` Did you mean "${suggestions[0]}"?` : '';
|
|
326
|
+
this.errors.push({
|
|
327
|
+
type: 'error',
|
|
328
|
+
code: 'UNKNOWN_TARGET_PORT',
|
|
329
|
+
message: `Exit node does not have input port "${toPort}"${suggestion}`,
|
|
330
|
+
node: toNode,
|
|
331
|
+
connection: conn,
|
|
332
|
+
location: connLocation,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Validate type compatibility for connections with coercion support
|
|
340
|
+
*
|
|
341
|
+
* Type coercion rules:
|
|
342
|
+
* - ANY type connections always allowed (no warning)
|
|
343
|
+
* - Safe coercions: NUMBER → STRING, BOOLEAN → STRING (no warning)
|
|
344
|
+
* - Lossy coercions: STRING → NUMBER, STRING → BOOLEAN, OBJECT → STRING (warning or error in strict mode)
|
|
345
|
+
* - Unusual coercions: NUMBER → BOOLEAN, BOOLEAN → NUMBER (warning or error in strict mode)
|
|
346
|
+
* - Same type connections always allowed (no warning)
|
|
347
|
+
*
|
|
348
|
+
* Strict types mode (@strictTypes):
|
|
349
|
+
* - When enabled, type incompatibilities are errors instead of warnings
|
|
350
|
+
*/
|
|
351
|
+
validateTypeCompatibility(workflow, instanceMap) {
|
|
352
|
+
const strictTypes = this.strictMode || workflow.options?.strictTypes === true;
|
|
353
|
+
// Helper to push to errors or warnings based on strictTypes mode
|
|
354
|
+
const pushTypeIssue = (issue, isIncompatible = false) => {
|
|
355
|
+
if (strictTypes && isIncompatible) {
|
|
356
|
+
this.errors.push({ ...issue, type: 'error', code: 'TYPE_INCOMPATIBLE' });
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
this.warnings.push(issue);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
workflow.connections.forEach((conn) => {
|
|
363
|
+
const fromNode = conn.from.node;
|
|
364
|
+
const fromPort = conn.from.port;
|
|
365
|
+
const toNode = conn.to.node;
|
|
366
|
+
const toPort = conn.to.port;
|
|
367
|
+
const connLocation = this.getConnectionLocation(conn);
|
|
368
|
+
// Skip Start and Exit nodes (they handle types dynamically)
|
|
369
|
+
if (isStartNode(fromNode) || isExitNode(toNode)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Get source and target port types
|
|
373
|
+
const sourceNode = instanceMap.get(fromNode);
|
|
374
|
+
const targetNode = instanceMap.get(toNode);
|
|
375
|
+
if (!sourceNode || !targetNode) {
|
|
376
|
+
return; // Already caught by validateConnections
|
|
377
|
+
}
|
|
378
|
+
const sourcePortDef = sourceNode.outputs[fromPort];
|
|
379
|
+
const targetPortDef = targetNode.inputs[toPort];
|
|
380
|
+
if (!sourcePortDef || !targetPortDef) {
|
|
381
|
+
return; // Already caught by validateConnections
|
|
382
|
+
}
|
|
383
|
+
const sourceType = sourcePortDef.dataType;
|
|
384
|
+
const targetType = targetPortDef.dataType;
|
|
385
|
+
// Validate STEP port connections - STEP must connect to STEP only
|
|
386
|
+
if (sourceType === 'STEP' && targetType !== 'STEP') {
|
|
387
|
+
this.errors.push({
|
|
388
|
+
type: 'error',
|
|
389
|
+
code: 'STEP_PORT_TYPE_MISMATCH',
|
|
390
|
+
message: `STEP port "${fromPort}" on node "${fromNode}" cannot connect to non-STEP port "${toPort}" (${targetType}) on node "${toNode}"`,
|
|
391
|
+
connection: conn,
|
|
392
|
+
location: connLocation,
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (targetType === 'STEP' && sourceType !== 'STEP') {
|
|
397
|
+
this.errors.push({
|
|
398
|
+
type: 'error',
|
|
399
|
+
code: 'STEP_PORT_TYPE_MISMATCH',
|
|
400
|
+
message: `Non-STEP port "${fromPort}" (${sourceType}) on node "${fromNode}" cannot connect to STEP port "${toPort}" on node "${toNode}"`,
|
|
401
|
+
connection: conn,
|
|
402
|
+
location: connLocation,
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// Skip further type checking for STEP-to-STEP (valid control flow)
|
|
407
|
+
if (sourceType === 'STEP' && targetType === 'STEP') {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Same type - check for structural compatibility if both are OBJECT
|
|
411
|
+
if (sourceType === targetType) {
|
|
412
|
+
// For OBJECT types, check if tsType differs (structural mismatch)
|
|
413
|
+
if (sourceType === 'OBJECT' && sourcePortDef.tsType && targetPortDef.tsType) {
|
|
414
|
+
// If both have tsType and they differ (after normalization), warn about potential mismatch
|
|
415
|
+
if (this.normalizeTypeString(sourcePortDef.tsType) !== this.normalizeTypeString(targetPortDef.tsType)) {
|
|
416
|
+
this.warnings.push({
|
|
417
|
+
type: 'warning',
|
|
418
|
+
code: 'OBJECT_TYPE_MISMATCH',
|
|
419
|
+
message: `Structural type mismatch: ${fromNode}.${fromPort} outputs "${sourcePortDef.tsType}" but ${toNode}.${toPort} expects "${targetPortDef.tsType}". Verify the object shapes are compatible.`,
|
|
420
|
+
connection: conn,
|
|
421
|
+
location: connLocation,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// ANY type - always compatible (no warning)
|
|
428
|
+
if (sourceType === 'ANY' || targetType === 'ANY') {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// Safe coercions (no warning)
|
|
432
|
+
const safeCoercions = [
|
|
433
|
+
['NUMBER', 'STRING'],
|
|
434
|
+
['BOOLEAN', 'STRING'],
|
|
435
|
+
];
|
|
436
|
+
for (const [from, to] of safeCoercions) {
|
|
437
|
+
if (sourceType === from && targetType === to) {
|
|
438
|
+
return; // Safe coercion, no warning
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Lossy coercions (warning)
|
|
442
|
+
const lossyCoercions = [
|
|
443
|
+
['STRING', 'NUMBER', 'May result in NaN if string is not a valid number'],
|
|
444
|
+
['STRING', 'BOOLEAN', 'Will use JavaScript truthy/falsy conversion'],
|
|
445
|
+
['OBJECT', 'STRING', 'Will use JSON.stringify()'],
|
|
446
|
+
['ARRAY', 'STRING', 'Will use JSON.stringify()'],
|
|
447
|
+
];
|
|
448
|
+
for (const [from, to, reason] of lossyCoercions) {
|
|
449
|
+
if (sourceType === from && targetType === to) {
|
|
450
|
+
pushTypeIssue({
|
|
451
|
+
type: 'warning',
|
|
452
|
+
code: 'LOSSY_TYPE_COERCION',
|
|
453
|
+
message: `Lossy type coercion from ${sourceType} to ${targetType} in connection ${fromNode}.${fromPort} → ${toNode}.${toPort}. ${reason}. Add @strictTypes to your workflow annotation to enforce type safety.`,
|
|
454
|
+
connection: conn,
|
|
455
|
+
location: connLocation,
|
|
456
|
+
}, true);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Unusual coercions (warning)
|
|
461
|
+
const unusualCoercions = [
|
|
462
|
+
[
|
|
463
|
+
'NUMBER',
|
|
464
|
+
'BOOLEAN',
|
|
465
|
+
'Will use JavaScript truthy/falsy conversion (0 = false, non-zero = true)',
|
|
466
|
+
],
|
|
467
|
+
['BOOLEAN', 'NUMBER', 'Will convert false to 0, true to 1'],
|
|
468
|
+
['STRING', 'OBJECT', 'May fail if string is not valid JSON'],
|
|
469
|
+
['STRING', 'ARRAY', 'May fail if string is not valid JSON array'],
|
|
470
|
+
];
|
|
471
|
+
for (const [from, to, reason] of unusualCoercions) {
|
|
472
|
+
if (sourceType === from && targetType === to) {
|
|
473
|
+
pushTypeIssue({
|
|
474
|
+
type: 'warning',
|
|
475
|
+
code: 'UNUSUAL_TYPE_COERCION',
|
|
476
|
+
message: `Unusual type coercion from ${sourceType} to ${targetType} in connection ${fromNode}.${fromPort} → ${toNode}.${toPort}. ${reason}.`,
|
|
477
|
+
connection: conn,
|
|
478
|
+
location: connLocation,
|
|
479
|
+
}, true);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// All other type mismatches
|
|
484
|
+
pushTypeIssue({
|
|
485
|
+
type: 'warning',
|
|
486
|
+
code: 'TYPE_MISMATCH',
|
|
487
|
+
message: `Type mismatch in connection ${fromNode}.${fromPort} (${sourceType}) → ${toNode}.${toPort} (${targetType}). Runtime coercion will be attempted.`,
|
|
488
|
+
connection: conn,
|
|
489
|
+
location: connLocation,
|
|
490
|
+
}, true);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
validateNodeReferences(workflow, instanceMap) {
|
|
494
|
+
const referencedNodes = new Set();
|
|
495
|
+
workflow.connections.forEach((conn) => {
|
|
496
|
+
const fromNode = conn.from.node;
|
|
497
|
+
const toNode = conn.to.node;
|
|
498
|
+
if (!isStartNode(fromNode) && !isExitNode(fromNode)) {
|
|
499
|
+
referencedNodes.add(fromNode);
|
|
500
|
+
}
|
|
501
|
+
if (!isStartNode(toNode) && !isExitNode(toNode)) {
|
|
502
|
+
referencedNodes.add(toNode);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
referencedNodes.forEach((nodeName) => {
|
|
506
|
+
if (!instanceMap.has(nodeName)) {
|
|
507
|
+
this.errors.push({
|
|
508
|
+
type: 'error',
|
|
509
|
+
code: 'UNDEFINED_NODE',
|
|
510
|
+
message: `Workflow references undefined node: "${nodeName}"`,
|
|
511
|
+
node: nodeName,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
validateRequiredInputs(workflow, instanceMap) {
|
|
517
|
+
instanceMap.forEach((nodeType, instanceId) => {
|
|
518
|
+
// Find the instance to check for port-level constant expressions
|
|
519
|
+
const instance = workflow.instances.find((inst) => inst.id === instanceId);
|
|
520
|
+
Object.entries(nodeType.inputs).forEach(([portName, portConfig]) => {
|
|
521
|
+
if (isExecutePort(portName))
|
|
522
|
+
return;
|
|
523
|
+
// Skip scoped INPUT ports - they're provided by scope function execution, not external connections
|
|
524
|
+
if (portConfig.scope)
|
|
525
|
+
return;
|
|
526
|
+
// Check if instance has an expression for this port
|
|
527
|
+
const instancePortConfig = instance?.config?.portConfigs?.find((pc) => pc.portName === portName && (pc.direction == null || pc.direction === 'INPUT'));
|
|
528
|
+
const hasInstanceExpression = instancePortConfig?.expression !== undefined;
|
|
529
|
+
const isRequired = !portConfig.optional &&
|
|
530
|
+
portConfig.default === undefined &&
|
|
531
|
+
!portConfig.expression &&
|
|
532
|
+
!hasInstanceExpression;
|
|
533
|
+
if (isRequired) {
|
|
534
|
+
const isConnected = workflow.connections.some((conn) => {
|
|
535
|
+
return conn.to.node === instanceId && conn.to.port === portName;
|
|
536
|
+
});
|
|
537
|
+
if (!isConnected) {
|
|
538
|
+
this.errors.push({
|
|
539
|
+
type: 'error',
|
|
540
|
+
code: 'MISSING_REQUIRED_INPUT',
|
|
541
|
+
message: `Node "${instanceId}" has unconnected required input port "${portName}". Connect a value to it, or mark it optional with @input [${portName}].`,
|
|
542
|
+
node: instanceId,
|
|
543
|
+
location: instance?.sourceLocation,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
detectUnusedNodes(workflow, instanceMap) {
|
|
551
|
+
const usedNodes = new Set();
|
|
552
|
+
workflow.connections.forEach((conn) => {
|
|
553
|
+
const fromNode = conn.from.node;
|
|
554
|
+
const toNode = conn.to.node;
|
|
555
|
+
if (!isStartNode(fromNode) && !isExitNode(fromNode)) {
|
|
556
|
+
usedNodes.add(fromNode);
|
|
557
|
+
}
|
|
558
|
+
if (!isStartNode(toNode) && !isExitNode(toNode)) {
|
|
559
|
+
usedNodes.add(toNode);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
instanceMap.forEach((_nodeType, instanceId) => {
|
|
563
|
+
if (!usedNodes.has(instanceId)) {
|
|
564
|
+
this.warnings.push({
|
|
565
|
+
type: 'warning',
|
|
566
|
+
code: 'UNUSED_NODE',
|
|
567
|
+
message: `Node "${instanceId}" is defined but never used in workflow`,
|
|
568
|
+
node: instanceId,
|
|
569
|
+
location: this.getInstanceLocation(workflow, instanceId),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
validateStartAndExit(workflow) {
|
|
575
|
+
const hasStartConnections = workflow.connections.some((conn) => {
|
|
576
|
+
return isStartNode(conn.from.node);
|
|
577
|
+
});
|
|
578
|
+
if (!hasStartConnections) {
|
|
579
|
+
this.warnings.push({
|
|
580
|
+
type: 'warning',
|
|
581
|
+
code: 'NO_START_CONNECTIONS',
|
|
582
|
+
message: 'Workflow has no connections from Start node',
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const hasExitConnections = workflow.connections.some((conn) => {
|
|
586
|
+
return isExitNode(conn.to.node);
|
|
587
|
+
});
|
|
588
|
+
if (!hasExitConnections) {
|
|
589
|
+
this.warnings.push({
|
|
590
|
+
type: 'warning',
|
|
591
|
+
code: 'NO_EXIT_CONNECTIONS',
|
|
592
|
+
message: 'Workflow has no connections to Exit node (no return value)',
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
// Validate that onSuccess and onFailure exit ports are STEP type
|
|
596
|
+
if (workflow.exitPorts.onSuccess) {
|
|
597
|
+
if (workflow.exitPorts.onSuccess.dataType !== 'STEP') {
|
|
598
|
+
this.errors.push({
|
|
599
|
+
type: 'error',
|
|
600
|
+
code: 'INVALID_EXIT_PORT_TYPE',
|
|
601
|
+
message: "Exit port 'onSuccess' must be of type STEP (control flow), found: " +
|
|
602
|
+
workflow.exitPorts.onSuccess.dataType,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (workflow.exitPorts.onFailure) {
|
|
607
|
+
if (workflow.exitPorts.onFailure.dataType !== 'STEP') {
|
|
608
|
+
this.errors.push({
|
|
609
|
+
type: 'error',
|
|
610
|
+
code: 'INVALID_EXIT_PORT_TYPE',
|
|
611
|
+
message: "Exit port 'onFailure' must be of type STEP (control flow), found: " +
|
|
612
|
+
workflow.exitPorts.onFailure.dataType,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Validate data flow in the workflow
|
|
619
|
+
*
|
|
620
|
+
* Checks for:
|
|
621
|
+
* - Unused output ports (data produced but never consumed)
|
|
622
|
+
* - Unreachable Exit ports (Exit expects data but no connection provides it)
|
|
623
|
+
* - Dead-end data paths (data that never reaches Exit)
|
|
624
|
+
*/
|
|
625
|
+
validateDataFlow(workflow, instanceMap) {
|
|
626
|
+
// Track which output ports are connected
|
|
627
|
+
const connectedOutputPorts = new Set();
|
|
628
|
+
workflow.connections.forEach((conn) => {
|
|
629
|
+
const portKey = `${conn.from.node}.${conn.from.port}`;
|
|
630
|
+
connectedOutputPorts.add(portKey);
|
|
631
|
+
});
|
|
632
|
+
// Check for unused output ports (excluding control flow ports and scoped ports)
|
|
633
|
+
instanceMap.forEach((nodeType, instanceId) => {
|
|
634
|
+
Object.keys(nodeType.outputs).forEach((portName) => {
|
|
635
|
+
const portKey = `${instanceId}.${portName}`;
|
|
636
|
+
const portDef = nodeType.outputs[portName];
|
|
637
|
+
// Skip control flow ports (onSuccess, onFailure)
|
|
638
|
+
if (portDef.isControlFlow || portDef.failure) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
// Skip scoped output ports - they flow through the scope function, not external connections
|
|
642
|
+
if (portDef.scope) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (!connectedOutputPorts.has(portKey)) {
|
|
646
|
+
this.warnings.push({
|
|
647
|
+
type: 'warning',
|
|
648
|
+
code: 'UNUSED_OUTPUT_PORT',
|
|
649
|
+
message: `Output port "${portName}" of node "${instanceId}" is never connected. Data will be discarded.`,
|
|
650
|
+
node: instanceId,
|
|
651
|
+
location: this.getInstanceLocation(workflow, instanceId),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
// Check for unreachable Exit ports and multiple connections to same Exit port
|
|
657
|
+
// Build a map of Exit ports to their incoming connections
|
|
658
|
+
const exitPortConnections = new Map();
|
|
659
|
+
workflow.connections.forEach((conn) => {
|
|
660
|
+
if (isExitNode(conn.to.node)) {
|
|
661
|
+
const port = conn.to.port;
|
|
662
|
+
if (!exitPortConnections.has(port)) {
|
|
663
|
+
exitPortConnections.set(port, []);
|
|
664
|
+
}
|
|
665
|
+
exitPortConnections.get(port).push(conn);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
// Check if all Exit ports have connections
|
|
669
|
+
Object.entries(workflow.exitPorts).forEach(([portName, portDef]) => {
|
|
670
|
+
// Skip control flow ports
|
|
671
|
+
if (portDef.isControlFlow) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (!exitPortConnections.has(portName)) {
|
|
675
|
+
this.warnings.push({
|
|
676
|
+
type: 'warning',
|
|
677
|
+
code: 'UNREACHABLE_EXIT_PORT',
|
|
678
|
+
message: `Exit port "${portName}" has no incoming connection. Return value will be undefined.`,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
// Check for multiple connections to the same Exit port
|
|
683
|
+
exitPortConnections.forEach((connections, portName) => {
|
|
684
|
+
if (connections.length > 1) {
|
|
685
|
+
const sourceNodes = connections.map((c) => c.from.node);
|
|
686
|
+
if (this.areMutuallyExclusive(sourceNodes, workflow, instanceMap)) {
|
|
687
|
+
return; // Suppress — mutually exclusive branches
|
|
688
|
+
}
|
|
689
|
+
const sources = connections.map((c) => `${c.from.node}.${c.from.port}`).join(', ');
|
|
690
|
+
this.warnings.push({
|
|
691
|
+
type: 'warning',
|
|
692
|
+
code: 'MULTIPLE_EXIT_CONNECTIONS',
|
|
693
|
+
message: `Exit port "${portName}" has ${connections.length} incoming connections (${sources}). Only one value will be used - consider using separate Exit ports.`,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Validate for cycles (loops) in the workflow graph.
|
|
700
|
+
* Cycles are connections that form a loop back to an earlier node.
|
|
701
|
+
* Scoped nodes handle loops internally, so cycles within same scope are checked.
|
|
702
|
+
*/
|
|
703
|
+
validateCycles(workflow) {
|
|
704
|
+
// Group instances by parent (scope layer)
|
|
705
|
+
const instancesByParent = new Map();
|
|
706
|
+
instancesByParent.set(null, []);
|
|
707
|
+
for (const instance of workflow.instances) {
|
|
708
|
+
const parentId = instance.parent?.id || null;
|
|
709
|
+
if (!instancesByParent.has(parentId)) {
|
|
710
|
+
instancesByParent.set(parentId, []);
|
|
711
|
+
}
|
|
712
|
+
instancesByParent.get(parentId).push(instance);
|
|
713
|
+
}
|
|
714
|
+
// Group connections by parent (only connections where both ends are in same scope)
|
|
715
|
+
const connectionsByParent = new Map();
|
|
716
|
+
connectionsByParent.set(null, []);
|
|
717
|
+
for (const connection of workflow.connections) {
|
|
718
|
+
const sourceInstance = workflow.instances.find((n) => n.id === connection.from.node);
|
|
719
|
+
const targetInstance = workflow.instances.find((n) => n.id === connection.to.node);
|
|
720
|
+
// Skip connections involving Start/Exit (they're virtual)
|
|
721
|
+
if (!sourceInstance || !targetInstance)
|
|
722
|
+
continue;
|
|
723
|
+
const sourceParent = sourceInstance.parent?.id || null;
|
|
724
|
+
const targetParent = targetInstance.parent?.id || null;
|
|
725
|
+
// Only check connections within the same scope
|
|
726
|
+
if (sourceParent === targetParent) {
|
|
727
|
+
if (!connectionsByParent.has(sourceParent)) {
|
|
728
|
+
connectionsByParent.set(sourceParent, []);
|
|
729
|
+
}
|
|
730
|
+
connectionsByParent.get(sourceParent).push(connection);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Run cycle detection per scope layer
|
|
734
|
+
for (const [parentId, instances] of instancesByParent.entries()) {
|
|
735
|
+
const connections = connectionsByParent.get(parentId) || [];
|
|
736
|
+
this.detectCyclesInLayer(parentId, instances, connections);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
detectCyclesInLayer(parentId, instances, connections) {
|
|
740
|
+
const visited = new Set();
|
|
741
|
+
const recursionStack = new Set();
|
|
742
|
+
const reportedCycles = new Set(); // Track reported cycles to avoid duplicates
|
|
743
|
+
// Self-loops are allowed (used for iteration patterns)
|
|
744
|
+
const selfLoopNodes = new Set(connections.filter((c) => c.from.node === c.to.node).map((c) => c.from.node));
|
|
745
|
+
// Exclude self-loops from cycle detection
|
|
746
|
+
const nonSelfLoopConnections = connections.filter((c) => c.from.node !== c.to.node);
|
|
747
|
+
const dfs = (nodeName, path) => {
|
|
748
|
+
if (recursionStack.has(nodeName)) {
|
|
749
|
+
// Node with self-loop is not considered a cycle entry point
|
|
750
|
+
if (selfLoopNodes.has(nodeName)) {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
const cycleStart = path.indexOf(nodeName);
|
|
754
|
+
const cyclePath = [...path.slice(cycleStart), nodeName];
|
|
755
|
+
// Normalize cycle for deduplication (start from smallest node name)
|
|
756
|
+
const cycleNodes = cyclePath.slice(0, -1); // Remove duplicate end node
|
|
757
|
+
const sortedCycle = [...cycleNodes].sort();
|
|
758
|
+
const cycleKey = sortedCycle.join(',');
|
|
759
|
+
// Only report if not already reported
|
|
760
|
+
if (!reportedCycles.has(cycleKey)) {
|
|
761
|
+
reportedCycles.add(cycleKey);
|
|
762
|
+
const parentContext = parentId ? ` in scope "${parentId}"` : '';
|
|
763
|
+
const instance = instances.find((n) => n.id === nodeName);
|
|
764
|
+
this.errors.push({
|
|
765
|
+
type: 'error',
|
|
766
|
+
code: 'CYCLE_DETECTED',
|
|
767
|
+
message: `Loop detected${parentContext}: ${cyclePath.join(' -> ')}`,
|
|
768
|
+
node: nodeName,
|
|
769
|
+
location: instance?.sourceLocation,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
if (visited.has(nodeName)) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
recursionStack.add(nodeName);
|
|
778
|
+
const newPath = [...path, nodeName];
|
|
779
|
+
const instance = instances.find((n) => n.id === nodeName);
|
|
780
|
+
if (!instance) {
|
|
781
|
+
recursionStack.delete(nodeName);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
const outgoing = nonSelfLoopConnections.filter((c) => c.from.node === nodeName);
|
|
785
|
+
let hasCycle = false;
|
|
786
|
+
for (const conn of outgoing) {
|
|
787
|
+
if (dfs(conn.to.node, newPath)) {
|
|
788
|
+
hasCycle = true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
recursionStack.delete(nodeName);
|
|
792
|
+
if (!hasCycle) {
|
|
793
|
+
visited.add(nodeName);
|
|
794
|
+
}
|
|
795
|
+
return hasCycle;
|
|
796
|
+
};
|
|
797
|
+
// Run DFS from each node (but use shared visited/reportedCycles sets)
|
|
798
|
+
for (const instance of instances) {
|
|
799
|
+
if (!visited.has(instance.id)) {
|
|
800
|
+
dfs(instance.id, []);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Validate that no input port has multiple connections.
|
|
806
|
+
* Only one value can be received per input port.
|
|
807
|
+
* (STEP ports can have multiple connections as they're control flow)
|
|
808
|
+
*/
|
|
809
|
+
validateMultipleInputConnections(workflow, instanceMap) {
|
|
810
|
+
const inputConnections = new Map();
|
|
811
|
+
for (const conn of workflow.connections) {
|
|
812
|
+
const targetKey = `${conn.to.node}.${conn.to.port}`;
|
|
813
|
+
// Skip Exit node (handled separately in validateDataFlow)
|
|
814
|
+
if (isExitNode(conn.to.node))
|
|
815
|
+
continue;
|
|
816
|
+
// Get target port type to check if it's STEP or has mergeStrategy
|
|
817
|
+
const targetNodeType = instanceMap.get(conn.to.node);
|
|
818
|
+
if (targetNodeType) {
|
|
819
|
+
const targetPortDef = targetNodeType.inputs[conn.to.port];
|
|
820
|
+
// STEP ports can have multiple connections (control flow)
|
|
821
|
+
if (targetPortDef?.dataType === 'STEP')
|
|
822
|
+
continue;
|
|
823
|
+
// Ports with mergeStrategy can have multiple connections (fan-in)
|
|
824
|
+
if (targetPortDef?.mergeStrategy)
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (!inputConnections.has(targetKey)) {
|
|
828
|
+
inputConnections.set(targetKey, []);
|
|
829
|
+
}
|
|
830
|
+
inputConnections.get(targetKey).push(conn);
|
|
831
|
+
}
|
|
832
|
+
for (const [, connections] of inputConnections) {
|
|
833
|
+
if (connections.length > 1) {
|
|
834
|
+
const [firstConn] = connections;
|
|
835
|
+
const sources = connections.map((c) => `${c.from.node}.${c.from.port}`).join(', ');
|
|
836
|
+
this.errors.push({
|
|
837
|
+
type: 'error',
|
|
838
|
+
code: 'MULTIPLE_CONNECTIONS_TO_INPUT',
|
|
839
|
+
message: `Input port "${firstConn.to.port}" on node "${firstConn.to.node}" has ${connections.length} connections (${sources}). Only one value can be received.`,
|
|
840
|
+
node: firstConn.to.node,
|
|
841
|
+
connection: firstConn,
|
|
842
|
+
location: this.getConnectionLocation(firstConn),
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Cross-check @input annotations against TypeScript function signatures.
|
|
849
|
+
* Warns on optionality and type mismatches between annotations and actual code.
|
|
850
|
+
*/
|
|
851
|
+
validateAnnotationSignatureConsistency(workflow) {
|
|
852
|
+
for (const nodeType of workflow.nodeTypes) {
|
|
853
|
+
if (!nodeType.functionText)
|
|
854
|
+
continue;
|
|
855
|
+
let sigParams;
|
|
856
|
+
try {
|
|
857
|
+
const sig = parseFunctionSignature(nodeType.functionText);
|
|
858
|
+
sigParams = sig.params;
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
continue; // Can't parse signature, skip
|
|
862
|
+
}
|
|
863
|
+
// Skip the first param (execute: boolean) - it's a control flow param
|
|
864
|
+
const sigParamMap = new Map();
|
|
865
|
+
for (const p of sigParams.slice(1)) {
|
|
866
|
+
sigParamMap.set(p.name, p);
|
|
867
|
+
}
|
|
868
|
+
for (const [portName, portDef] of Object.entries(nodeType.inputs)) {
|
|
869
|
+
if (portName === 'execute')
|
|
870
|
+
continue; // Skip control flow port
|
|
871
|
+
const sigParam = sigParamMap.get(portName);
|
|
872
|
+
if (!sigParam)
|
|
873
|
+
continue; // Port not in signature (may be added by framework)
|
|
874
|
+
// Check optionality mismatch: annotation says required but sig says optional
|
|
875
|
+
if (!portDef.optional && sigParam.optional) {
|
|
876
|
+
this.warnings.push({
|
|
877
|
+
type: 'warning',
|
|
878
|
+
code: 'ANNOTATION_SIGNATURE_MISMATCH',
|
|
879
|
+
message: `Port "${portName}" in node type "${nodeType.functionName}" is optional in signature but required in annotation. Consider using @input [${portName}] to mark it optional.`,
|
|
880
|
+
node: nodeType.functionName,
|
|
881
|
+
location: nodeType.sourceLocation,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
// Check type mismatch: annotation specifies a type that differs from signature
|
|
885
|
+
if (portDef.tsType && sigParam.tsType) {
|
|
886
|
+
const annotationType = this.normalizeTypeString(portDef.tsType);
|
|
887
|
+
const signatureType = this.normalizeTypeString(sigParam.tsType);
|
|
888
|
+
// Skip if no real type info (untyped JS)
|
|
889
|
+
if (!signatureType || signatureType === 'any')
|
|
890
|
+
continue;
|
|
891
|
+
if (annotationType !== signatureType) {
|
|
892
|
+
this.warnings.push({
|
|
893
|
+
type: 'warning',
|
|
894
|
+
code: 'ANNOTATION_SIGNATURE_TYPE_MISMATCH',
|
|
895
|
+
message: `Port "${portName}" in node type "${nodeType.functionName}" has type "${portDef.tsType}" in annotation but "${sigParam.tsType}" in function signature.`,
|
|
896
|
+
node: nodeType.functionName,
|
|
897
|
+
location: nodeType.sourceLocation,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Check if a set of source nodes are mutually exclusive — i.e., they descend
|
|
906
|
+
* from opposite branches (onSuccess vs onFailure) of the same branching node.
|
|
907
|
+
*/
|
|
908
|
+
areMutuallyExclusive(sourceNodes, workflow, instanceMap) {
|
|
909
|
+
if (sourceNodes.length < 2)
|
|
910
|
+
return false;
|
|
911
|
+
// Build reverse connection map: targetNode -> [{fromNode, fromPort}]
|
|
912
|
+
const reverseMap = new Map();
|
|
913
|
+
for (const conn of workflow.connections) {
|
|
914
|
+
if (!reverseMap.has(conn.to.node)) {
|
|
915
|
+
reverseMap.set(conn.to.node, []);
|
|
916
|
+
}
|
|
917
|
+
reverseMap.get(conn.to.node).push({ fromNode: conn.from.node, fromPort: conn.from.port });
|
|
918
|
+
}
|
|
919
|
+
const findBranchAncestor = (nodeId) => {
|
|
920
|
+
const visited = new Set();
|
|
921
|
+
const queue = [nodeId];
|
|
922
|
+
while (queue.length > 0) {
|
|
923
|
+
const current = queue.shift();
|
|
924
|
+
if (visited.has(current))
|
|
925
|
+
continue;
|
|
926
|
+
visited.add(current);
|
|
927
|
+
const incomingEdges = reverseMap.get(current);
|
|
928
|
+
if (!incomingEdges)
|
|
929
|
+
continue;
|
|
930
|
+
for (const edge of incomingEdges) {
|
|
931
|
+
// Check if this incoming edge is from a branching port
|
|
932
|
+
if (edge.fromPort === 'onSuccess' || edge.fromPort === 'onFailure') {
|
|
933
|
+
const parentNodeType = instanceMap.get(edge.fromNode);
|
|
934
|
+
if (parentNodeType?.hasSuccessPort && parentNodeType?.hasFailurePort) {
|
|
935
|
+
return { branchNode: edge.fromNode, branch: edge.fromPort };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
queue.push(edge.fromNode);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return null;
|
|
942
|
+
};
|
|
943
|
+
// Get branch info for all source nodes
|
|
944
|
+
const branchInfos = sourceNodes.map(findBranchAncestor);
|
|
945
|
+
// All must have a branch ancestor
|
|
946
|
+
if (branchInfos.some((info) => info === null))
|
|
947
|
+
return false;
|
|
948
|
+
// All must share the same branch node
|
|
949
|
+
const branchNode = branchInfos[0].branchNode;
|
|
950
|
+
if (!branchInfos.every((info) => info.branchNode === branchNode))
|
|
951
|
+
return false;
|
|
952
|
+
// They must be on different branches (not all on the same one)
|
|
953
|
+
const branches = new Set(branchInfos.map((info) => info.branch));
|
|
954
|
+
return branches.size > 1;
|
|
955
|
+
}
|
|
956
|
+
normalizeTypeString(type) {
|
|
957
|
+
let n = type;
|
|
958
|
+
// Remove all whitespace
|
|
959
|
+
n = n.replace(/\s+/g, '');
|
|
960
|
+
// Normalize Array<T> → T[]
|
|
961
|
+
n = n.replace(/Array<(.+?)>/g, '$1[]');
|
|
962
|
+
// Remove trailing semicolons before closing braces/brackets
|
|
963
|
+
n = n.replace(/;(?=[}\]])/g, '');
|
|
964
|
+
// Lowercase for case-insensitive compare
|
|
965
|
+
n = n.toLowerCase();
|
|
966
|
+
return n;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
export const validator = new WorkflowValidator();
|
|
970
|
+
//# sourceMappingURL=validator.js.map
|