@path58/n8n-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_INSTALL.md +223 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/autofix/suggestion-fixers/deprecated-node-fixer.js +465 -0
- package/dist/autofix/suggestion-fixers/deprecated-node-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/fixer-registry.js +495 -0
- package/dist/autofix/suggestion-fixers/fixer-registry.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l1-structure-fixer.js +639 -0
- package/dist/autofix/suggestion-fixers/l1-structure-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l4-connection-fixer.js +449 -0
- package/dist/autofix/suggestion-fixers/l4-connection-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l5-parameter-fixer.js +575 -0
- package/dist/autofix/suggestion-fixers/l5-parameter-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l5-typeversion-fixer.js +431 -0
- package/dist/autofix/suggestion-fixers/l5-typeversion-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l5-webhook-path-fixer.js +356 -0
- package/dist/autofix/suggestion-fixers/l5-webhook-path-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l6-ai-tool-variant-fixer.js +618 -0
- package/dist/autofix/suggestion-fixers/l6-ai-tool-variant-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l6-pattern-fixer.js +1475 -0
- package/dist/autofix/suggestion-fixers/l6-pattern-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/llm-fixer.js +716 -0
- package/dist/autofix/suggestion-fixers/llm-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/missing-credential-fixer.js +336 -0
- package/dist/autofix/suggestion-fixers/missing-credential-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/types.js +29 -0
- package/dist/autofix/suggestion-fixers/types.js.map +1 -0
- package/dist/autofix/suggestion-fixers/typo-fixer.js +197 -0
- package/dist/autofix/suggestion-fixers/typo-fixer.js.map +1 -0
- package/dist/classification/certification-engine.js +208 -0
- package/dist/classification/certification-engine.js.map +1 -0
- package/dist/classification/feedback-collector.js +516 -0
- package/dist/classification/feedback-collector.js.map +1 -0
- package/dist/classification/l5-parameter-analyzer.js +670 -0
- package/dist/classification/l5-parameter-analyzer.js.map +1 -0
- package/dist/classification/l6-graph-analyzer.js +613 -0
- package/dist/classification/l6-graph-analyzer.js.map +1 -0
- package/dist/classification/severity-classifier.js +237 -0
- package/dist/classification/severity-classifier.js.map +1 -0
- package/dist/config/env.js +280 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/env.schema.js +234 -0
- package/dist/config/env.schema.js.map +1 -0
- package/dist/config/scraperEnv.js +55 -0
- package/dist/config/scraperEnv.js.map +1 -0
- package/dist/db/postgresClient.js +38 -0
- package/dist/db/postgresClient.js.map +1 -0
- package/dist/db/scraperDb.js +6 -0
- package/dist/db/scraperDb.js.map +1 -0
- package/dist/db/scraperPostgresClient.js +118 -0
- package/dist/db/scraperPostgresClient.js.map +1 -0
- package/dist/db/validationRepository.js +55 -0
- package/dist/db/validationRepository.js.map +1 -0
- package/dist/db/validatorPostgresClient.js +248 -0
- package/dist/db/validatorPostgresClient.js.map +1 -0
- package/dist/db/workflowInstanceMappingRepository.js +128 -0
- package/dist/db/workflowInstanceMappingRepository.js.map +1 -0
- package/dist/errors/AppError.js +156 -0
- package/dist/errors/AppError.js.map +1 -0
- package/dist/errors/index.js +7 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/factory/error-to-problem-mappers.js +385 -0
- package/dist/factory/error-to-problem-mappers.js.map +1 -0
- package/dist/factory/gap-recorder.js +260 -0
- package/dist/factory/gap-recorder.js.map +1 -0
- package/dist/factory/problem-recorder.js +94 -0
- package/dist/factory/problem-recorder.js.map +1 -0
- package/dist/factory/warning-to-gap-mappers.js +493 -0
- package/dist/factory/warning-to-gap-mappers.js.map +1 -0
- package/dist/factory/workflow-normalizer.js +247 -0
- package/dist/factory/workflow-normalizer.js.map +1 -0
- package/dist/mcp/adapters/catalog.js +13 -0
- package/dist/mcp/adapters/catalog.js.map +1 -0
- package/dist/mcp/adapters/index.js +36 -0
- package/dist/mcp/adapters/index.js.map +1 -0
- package/dist/mcp/adapters/supabase-catalog.js +467 -0
- package/dist/mcp/adapters/supabase-catalog.js.map +1 -0
- package/dist/mcp/adapters/test-catalog-adapter.js +100 -0
- package/dist/mcp/adapters/test-catalog-adapter.js.map +1 -0
- package/dist/mcp/adapters/validation.js +258 -0
- package/dist/mcp/adapters/validation.js.map +1 -0
- package/dist/mcp/build-email-workflow.js +113 -0
- package/dist/mcp/build-email-workflow.js.map +1 -0
- package/dist/mcp/config.js +22 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/formatters/errors.js +217 -0
- package/dist/mcp/formatters/errors.js.map +1 -0
- package/dist/mcp/formatters/index.js +12 -0
- package/dist/mcp/formatters/index.js.map +1 -0
- package/dist/mcp/formatters/response.js +141 -0
- package/dist/mcp/formatters/response.js.map +1 -0
- package/dist/mcp/quick-test.js +33 -0
- package/dist/mcp/quick-test.js.map +1 -0
- package/dist/mcp/server.js +70 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/test-mcp-error.js +81 -0
- package/dist/mcp/test-mcp-error.js.map +1 -0
- package/dist/mcp/test-mcp.js +80 -0
- package/dist/mcp/test-mcp.js.map +1 -0
- package/dist/mcp/tools/fixes/expression-fixes.js +166 -0
- package/dist/mcp/tools/fixes/expression-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/flow-fixes.js +155 -0
- package/dist/mcp/tools/fixes/flow-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/index.js +91 -0
- package/dist/mcp/tools/fixes/index.js.map +1 -0
- package/dist/mcp/tools/fixes/node-fixes.js +233 -0
- package/dist/mcp/tools/fixes/node-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/parameter-fixes.js +277 -0
- package/dist/mcp/tools/fixes/parameter-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/types.js +10 -0
- package/dist/mcp/tools/fixes/types.js.map +1 -0
- package/dist/mcp/tools/handlers/check-parameter.js +300 -0
- package/dist/mcp/tools/handlers/check-parameter.js.map +1 -0
- package/dist/mcp/tools/handlers/find-similar-pattern.js +121 -0
- package/dist/mcp/tools/handlers/find-similar-pattern.js.map +1 -0
- package/dist/mcp/tools/handlers/get-node-info.js +131 -0
- package/dist/mcp/tools/handlers/get-node-info.js.map +1 -0
- package/dist/mcp/tools/handlers/get-operation-schema.js +141 -0
- package/dist/mcp/tools/handlers/get-operation-schema.js.map +1 -0
- package/dist/mcp/tools/handlers/list-nodes.js +126 -0
- package/dist/mcp/tools/handlers/list-nodes.js.map +1 -0
- package/dist/mcp/tools/handlers/list-operations.js +138 -0
- package/dist/mcp/tools/handlers/list-operations.js.map +1 -0
- package/dist/mcp/tools/handlers/suggest-fix.js +120 -0
- package/dist/mcp/tools/handlers/suggest-fix.js.map +1 -0
- package/dist/mcp/tools/handlers/validate-workflow.js +92 -0
- package/dist/mcp/tools/handlers/validate-workflow.js.map +1 -0
- package/dist/mcp/tools/index.js +190 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/schemas.js +195 -0
- package/dist/mcp/tools/schemas.js.map +1 -0
- package/dist/mcp/tools/validate.js +95 -0
- package/dist/mcp/tools/validate.js.map +1 -0
- package/dist/mcp/types/mcp.js +7 -0
- package/dist/mcp/types/mcp.js.map +1 -0
- package/dist/mcp/utils/timeout.js +78 -0
- package/dist/mcp/utils/timeout.js.map +1 -0
- package/dist/services/BatchProcessor.js +433 -0
- package/dist/services/BatchProcessor.js.map +1 -0
- package/dist/services/CheckpointManager.js +281 -0
- package/dist/services/CheckpointManager.js.map +1 -0
- package/dist/services/CostCalculator.js +211 -0
- package/dist/services/CostCalculator.js.map +1 -0
- package/dist/services/EmbeddingCache.js +68 -0
- package/dist/services/EmbeddingCache.js.map +1 -0
- package/dist/services/EmbeddingService.js +143 -0
- package/dist/services/EmbeddingService.js.map +1 -0
- package/dist/services/RankingService.js +81 -0
- package/dist/services/RankingService.js.map +1 -0
- package/dist/services/RedisCache.js +376 -0
- package/dist/services/RedisCache.js.map +1 -0
- package/dist/services/RedisCatalogCache.js +680 -0
- package/dist/services/RedisCatalogCache.js.map +1 -0
- package/dist/services/ResumeManager.js +252 -0
- package/dist/services/ResumeManager.js.map +1 -0
- package/dist/services/SearchService.js +282 -0
- package/dist/services/SearchService.js.map +1 -0
- package/dist/services/SemanticCatalogSearch.js +405 -0
- package/dist/services/SemanticCatalogSearch.js.map +1 -0
- package/dist/services/ValidationCache.js +157 -0
- package/dist/services/ValidationCache.js.map +1 -0
- package/dist/services/WorkflowPipelineService.js +1997 -0
- package/dist/services/WorkflowPipelineService.js.map +1 -0
- package/dist/services/catalog/index.js +34 -0
- package/dist/services/catalog/index.js.map +1 -0
- package/dist/services/catalog/interfaces.js +17 -0
- package/dist/services/catalog/interfaces.js.map +1 -0
- package/dist/services/catalog/loaders.js +169 -0
- package/dist/services/catalog/loaders.js.map +1 -0
- package/dist/services/catalog/types.js +138 -0
- package/dist/services/catalog/types.js.map +1 -0
- package/dist/services/documentation-normalization/docUrlUtils.js +88 -0
- package/dist/services/documentation-normalization/docUrlUtils.js.map +1 -0
- package/dist/services/error-quality/ErrorQualityService.js +262 -0
- package/dist/services/error-quality/ErrorQualityService.js.map +1 -0
- package/dist/services/error-quality/analyzers/CredentialAnalyzer.js +260 -0
- package/dist/services/error-quality/analyzers/CredentialAnalyzer.js.map +1 -0
- package/dist/services/error-quality/analyzers/IssuePredictor.js +380 -0
- package/dist/services/error-quality/analyzers/IssuePredictor.js.map +1 -0
- package/dist/services/error-quality/analyzers/MockCoverageAnalyzer.js +267 -0
- package/dist/services/error-quality/analyzers/MockCoverageAnalyzer.js.map +1 -0
- package/dist/services/error-quality/data/ErrorPatternSeeder.js +963 -0
- package/dist/services/error-quality/data/ErrorPatternSeeder.js.map +1 -0
- package/dist/services/error-quality/index.js +25 -0
- package/dist/services/error-quality/index.js.map +1 -0
- package/dist/services/error-quality/reports/ReportGenerator.js +343 -0
- package/dist/services/error-quality/reports/ReportGenerator.js.map +1 -0
- package/dist/services/error-quality/taxonomy/ErrorTaxonomy.js +698 -0
- package/dist/services/error-quality/taxonomy/ErrorTaxonomy.js.map +1 -0
- package/dist/services/error-quality/types.js +11 -0
- package/dist/services/error-quality/types.js.map +1 -0
- package/dist/services/progress/ProgressTracker.js +288 -0
- package/dist/services/progress/ProgressTracker.js.map +1 -0
- package/dist/services/progress/formatters.js +122 -0
- package/dist/services/progress/formatters.js.map +1 -0
- package/dist/services/progress/index.js +36 -0
- package/dist/services/progress/index.js.map +1 -0
- package/dist/services/progress/types.js +7 -0
- package/dist/services/progress/types.js.map +1 -0
- package/dist/services/search/embeddingGenerator.js +112 -0
- package/dist/services/search/embeddingGenerator.js.map +1 -0
- package/dist/types/aiCapabilities.js +7 -0
- package/dist/types/aiCapabilities.js.map +1 -0
- package/dist/types/aiConfigSchema.js +7 -0
- package/dist/types/aiConfigSchema.js.map +1 -0
- package/dist/utils/bannerLogger.js +186 -0
- package/dist/utils/bannerLogger.js.map +1 -0
- package/dist/utils/bannerService.js +23 -0
- package/dist/utils/bannerService.js.map +1 -0
- package/dist/utils/bannerServiceAdapter.js +54 -0
- package/dist/utils/bannerServiceAdapter.js.map +1 -0
- package/dist/utils/batchLogger.js +171 -0
- package/dist/utils/batchLogger.js.map +1 -0
- package/dist/utils/bottomStickyBanner.js +239 -0
- package/dist/utils/bottomStickyBanner.js.map +1 -0
- package/dist/utils/credentialMatcher.js +206 -0
- package/dist/utils/credentialMatcher.js.map +1 -0
- package/dist/utils/credentialNormalizer.js +442 -0
- package/dist/utils/credentialNormalizer.js.map +1 -0
- package/dist/utils/integratedBannerLogger.js +59 -0
- package/dist/utils/integratedBannerLogger.js.map +1 -0
- package/dist/utils/n8nSourceGit.js +195 -0
- package/dist/utils/n8nSourceGit.js.map +1 -0
- package/dist/utils/nodeTypeNormalizer.js +131 -0
- package/dist/utils/nodeTypeNormalizer.js.map +1 -0
- package/dist/utils/openaiClient.js +397 -0
- package/dist/utils/openaiClient.js.map +1 -0
- package/dist/utils/productionLogger.js +16 -0
- package/dist/utils/productionLogger.js.map +1 -0
- package/dist/utils/progressBarBanner.js +132 -0
- package/dist/utils/progressBarBanner.js.map +1 -0
- package/dist/utils/scriptHeartbeat.js +117 -0
- package/dist/utils/scriptHeartbeat.js.map +1 -0
- package/dist/utils/scriptLogger.js +125 -0
- package/dist/utils/scriptLogger.js.map +1 -0
- package/dist/utils/scriptRunner.js +95 -0
- package/dist/utils/scriptRunner.js.map +1 -0
- package/dist/utils/scriptTimeout.js +128 -0
- package/dist/utils/scriptTimeout.js.map +1 -0
- package/dist/utils/scriptWrapper.js +219 -0
- package/dist/utils/scriptWrapper.js.map +1 -0
- package/dist/utils/stickyBanner.js +226 -0
- package/dist/utils/stickyBanner.js.map +1 -0
- package/dist/utils/terminalSpinner.js +97 -0
- package/dist/utils/terminalSpinner.js.map +1 -0
- package/dist/utils/threeLineBanner.js +427 -0
- package/dist/utils/threeLineBanner.js.map +1 -0
- package/dist/utils/validatorCheckpointManager.js +170 -0
- package/dist/utils/validatorCheckpointManager.js.map +1 -0
- package/dist/utils/validatorConnectionManager.js +124 -0
- package/dist/utils/validatorConnectionManager.js.map +1 -0
- package/dist/validation/catalog.js +56 -0
- package/dist/validation/catalog.js.map +1 -0
- package/dist/validation/config/deprecated-nodes.js +234 -0
- package/dist/validation/config/deprecated-nodes.js.map +1 -0
- package/dist/validation/config/l6-severity.js +227 -0
- package/dist/validation/config/l6-severity.js.map +1 -0
- package/dist/validation/config/terminal-nodes.js +132 -0
- package/dist/validation/config/terminal-nodes.js.map +1 -0
- package/dist/validation/config/unreachable-nodes.js +67 -0
- package/dist/validation/config/unreachable-nodes.js.map +1 -0
- package/dist/validation/core.js +47 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/docExtraction.js +12 -0
- package/dist/validation/docExtraction.js.map +1 -0
- package/dist/validation/dryRunMockRunner.js +128 -0
- package/dist/validation/dryRunMockRunner.js.map +1 -0
- package/dist/validation/fixtureEngine.js +61 -0
- package/dist/validation/fixtureEngine.js.map +1 -0
- package/dist/validation/index.js +15 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/k-levels/k2-blockers.js +222 -0
- package/dist/validation/k-levels/k2-blockers.js.map +1 -0
- package/dist/validation/l1-structure.js +296 -0
- package/dist/validation/l1-structure.js.map +1 -0
- package/dist/validation/l2-nodes.js +282 -0
- package/dist/validation/l2-nodes.js.map +1 -0
- package/dist/validation/l3-credentials.js +322 -0
- package/dist/validation/l3-credentials.js.map +1 -0
- package/dist/validation/l4-connections.js +698 -0
- package/dist/validation/l4-connections.js.map +1 -0
- package/dist/validation/l5-parameters.js +803 -0
- package/dist/validation/l5-parameters.js.map +1 -0
- package/dist/validation/l6-checks/ai-tool-variants.js +407 -0
- package/dist/validation/l6-checks/ai-tool-variants.js.map +1 -0
- package/dist/validation/l6-checks/catalog-checks.js +260 -0
- package/dist/validation/l6-checks/catalog-checks.js.map +1 -0
- package/dist/validation/l6-checks/data-contracts.js +197 -0
- package/dist/validation/l6-checks/data-contracts.js.map +1 -0
- package/dist/validation/l6-checks/deprecation.js +133 -0
- package/dist/validation/l6-checks/deprecation.js.map +1 -0
- package/dist/validation/l6-checks/error-handling.js +193 -0
- package/dist/validation/l6-checks/error-handling.js.map +1 -0
- package/dist/validation/l6-checks/expression-syntax.js +387 -0
- package/dist/validation/l6-checks/expression-syntax.js.map +1 -0
- package/dist/validation/l6-checks/flow-integrity.js +504 -0
- package/dist/validation/l6-checks/flow-integrity.js.map +1 -0
- package/dist/validation/l6-checks/index.js +106 -0
- package/dist/validation/l6-checks/index.js.map +1 -0
- package/dist/validation/l6-checks/loops.js +370 -0
- package/dist/validation/l6-checks/loops.js.map +1 -0
- package/dist/validation/l6-checks/performance.js +182 -0
- package/dist/validation/l6-checks/performance.js.map +1 -0
- package/dist/validation/l6-checks/security.js +273 -0
- package/dist/validation/l6-checks/security.js.map +1 -0
- package/dist/validation/l6-patterns.js +472 -0
- package/dist/validation/l6-patterns.js.map +1 -0
- package/dist/validation/mockLevelResolver.js +95 -0
- package/dist/validation/mockLevelResolver.js.map +1 -0
- package/dist/validation/n8nApiClient.js +21 -0
- package/dist/validation/n8nApiClient.js.map +1 -0
- package/dist/validation/n8nCli.js +87 -0
- package/dist/validation/n8nCli.js.map +1 -0
- package/dist/validation/types.js +8 -0
- package/dist/validation/types.js.map +1 -0
- package/dist/validation/usageStats.js +82 -0
- package/dist/validation/usageStats.js.map +1 -0
- package/package.json +274 -0
|
@@ -0,0 +1,1475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L6 Pattern Fixers
|
|
3
|
+
*
|
|
4
|
+
* RAG-2.2.93.9: L6 Pattern Fixers
|
|
5
|
+
* Fixes L6 pattern validation problems from validation_problems:
|
|
6
|
+
* - L6_NO_RETRY_ON_HTTP: Adds retry configuration to HTTP nodes
|
|
7
|
+
* - L6_DEAD_END_NODE: Connects dead ends to output or error handler
|
|
8
|
+
* - L6_ERROR_OUTPUT_UNCONNECTED: Connects error outputs or enables continueOnFail
|
|
9
|
+
* - L6_INVALID_EXPRESSION_SYNTAX: Fixes unbalanced braces
|
|
10
|
+
* - L6_UNBALANCED_BRACES: Balances {{ }} braces in expressions
|
|
11
|
+
* - L6_NO_TRIGGER: Adds manual trigger when missing
|
|
12
|
+
* - L6_HARDCODED_URL: Records as gap (not auto-fixable)
|
|
13
|
+
*
|
|
14
|
+
* @module suggestion-fixers/l6-pattern-fixer
|
|
15
|
+
* @created 2026-01-23
|
|
16
|
+
*/
|
|
17
|
+
import { cloneWorkflow } from './types.js';
|
|
18
|
+
import { logger } from '@tsvika58/shared-utilities/logging';
|
|
19
|
+
/**
|
|
20
|
+
* Create a failure result with consistent format.
|
|
21
|
+
*/
|
|
22
|
+
function createFailureResult(fixerName, problemCode, reason) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
changes: [],
|
|
26
|
+
reason,
|
|
27
|
+
confidence: 0,
|
|
28
|
+
fixer_name: fixerName,
|
|
29
|
+
problem_code: problemCode,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a success result with consistent format.
|
|
34
|
+
*/
|
|
35
|
+
function createSuccessResult(fixerName, problemCode, workflow, changes, confidence) {
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
fixedWorkflow: workflow,
|
|
39
|
+
changes,
|
|
40
|
+
confidence,
|
|
41
|
+
fixer_name: fixerName,
|
|
42
|
+
problem_code: problemCode,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Find a node by ID or name in the workflow.
|
|
47
|
+
*/
|
|
48
|
+
function findNode(workflow, nodeId, nodeName) {
|
|
49
|
+
if (nodeId) {
|
|
50
|
+
const nodeById = workflow.nodes.find((n) => n.id === nodeId);
|
|
51
|
+
if (nodeById)
|
|
52
|
+
return nodeById;
|
|
53
|
+
}
|
|
54
|
+
if (nodeName) {
|
|
55
|
+
return workflow.nodes.find((n) => n.name === nodeName);
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract node name from problem description.
|
|
61
|
+
* Handles patterns like:
|
|
62
|
+
* - HTTP node "Download Audio" has no retry configuration
|
|
63
|
+
* - Node "Some Name" is a dead end
|
|
64
|
+
*/
|
|
65
|
+
function extractNodeNameFromDescription(description) {
|
|
66
|
+
if (!description)
|
|
67
|
+
return undefined;
|
|
68
|
+
// Match quoted node name: "NodeName"
|
|
69
|
+
const quotedMatch = description.match(/(?:node|Node)\s+"([^"]+)"/);
|
|
70
|
+
if (quotedMatch) {
|
|
71
|
+
return quotedMatch[1];
|
|
72
|
+
}
|
|
73
|
+
// Match single-quoted node name: 'NodeName'
|
|
74
|
+
const singleQuotedMatch = description.match(/(?:node|Node)\s+'([^']+)'/);
|
|
75
|
+
if (singleQuotedMatch) {
|
|
76
|
+
return singleQuotedMatch[1];
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Generate a unique node ID.
|
|
82
|
+
*/
|
|
83
|
+
function generateNodeId() {
|
|
84
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
85
|
+
}
|
|
86
|
+
// ============================================================
|
|
87
|
+
// L6 RETRY FIXER
|
|
88
|
+
// ============================================================
|
|
89
|
+
/**
|
|
90
|
+
* L6RetryFixer - Adds retry configuration to HTTP nodes
|
|
91
|
+
*
|
|
92
|
+
* Handles L6_NO_RETRY_ON_HTTP problems by:
|
|
93
|
+
* 1. Finding the HTTP node
|
|
94
|
+
* 2. Adding standard retry configuration
|
|
95
|
+
*
|
|
96
|
+
* Confidence: 0.90 (HIGH - standard retry config is safe)
|
|
97
|
+
* Auto-applicable: Yes - adding retry doesn't change workflow semantics
|
|
98
|
+
*/
|
|
99
|
+
export class L6RetryFixer {
|
|
100
|
+
name = 'L6RetryFixer';
|
|
101
|
+
description = 'Adds retry configuration to HTTP nodes';
|
|
102
|
+
supportedCodes = ['L6_NO_RETRY_ON_HTTP'];
|
|
103
|
+
minConfidence = 0.90;
|
|
104
|
+
canFix(problem) {
|
|
105
|
+
return problem.level === 'L6' && problem.code === 'L6_NO_RETRY_ON_HTTP';
|
|
106
|
+
}
|
|
107
|
+
fix(workflow, problem) {
|
|
108
|
+
if (!this.canFix(problem)) {
|
|
109
|
+
return createFailureResult(this.name, problem.code, 'Problem does not meet requirements for L6RetryFixer');
|
|
110
|
+
}
|
|
111
|
+
// Try to find node by id/name, or fallback to extracting from description
|
|
112
|
+
const nodeNameFromDesc = extractNodeNameFromDescription(problem.description);
|
|
113
|
+
const node = findNode(workflow, problem.node_id, problem.node_name) ||
|
|
114
|
+
(nodeNameFromDesc ? findNode(workflow, undefined, nodeNameFromDesc) : undefined);
|
|
115
|
+
if (!node) {
|
|
116
|
+
return createFailureResult(this.name, problem.code, `Node not found: ${problem.node_id || problem.node_name || nodeNameFromDesc || 'unknown'}`);
|
|
117
|
+
}
|
|
118
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
119
|
+
const targetNode = findNode(fixedWorkflow, node.id, node.name);
|
|
120
|
+
// Store old values for change tracking
|
|
121
|
+
const oldRetryOnFail = targetNode.retryOnFail;
|
|
122
|
+
const oldMaxTries = targetNode.maxTries;
|
|
123
|
+
const oldWaitBetweenTries = targetNode.waitBetweenTries;
|
|
124
|
+
const oldOnError = targetNode.onError;
|
|
125
|
+
// Add retry configuration
|
|
126
|
+
targetNode.retryOnFail = true;
|
|
127
|
+
targetNode.maxTries = 3;
|
|
128
|
+
targetNode.waitBetweenTries = 1000;
|
|
129
|
+
targetNode.onError = 'continueErrorOutput';
|
|
130
|
+
const change = {
|
|
131
|
+
change_type: 'parameter_update',
|
|
132
|
+
node_id: targetNode.id,
|
|
133
|
+
node_name: targetNode.name,
|
|
134
|
+
field: 'retryConfig',
|
|
135
|
+
old_value: {
|
|
136
|
+
retryOnFail: oldRetryOnFail,
|
|
137
|
+
maxTries: oldMaxTries,
|
|
138
|
+
waitBetweenTries: oldWaitBetweenTries,
|
|
139
|
+
onError: oldOnError,
|
|
140
|
+
},
|
|
141
|
+
new_value: {
|
|
142
|
+
retryOnFail: true,
|
|
143
|
+
maxTries: 3,
|
|
144
|
+
waitBetweenTries: 1000,
|
|
145
|
+
onError: 'continueErrorOutput',
|
|
146
|
+
},
|
|
147
|
+
confidence: 0.90,
|
|
148
|
+
description: `Added retry configuration to HTTP node "${targetNode.name}" (3 retries, 1000ms delay)`,
|
|
149
|
+
};
|
|
150
|
+
logger.info('L6RetryFixer applied fix', {
|
|
151
|
+
node_name: targetNode.name,
|
|
152
|
+
problem_code: problem.code,
|
|
153
|
+
});
|
|
154
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], 0.90);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ============================================================
|
|
158
|
+
// L6 ERROR OUTPUT FIXER
|
|
159
|
+
// ============================================================
|
|
160
|
+
/**
|
|
161
|
+
* L6ErrorOutputFixer - Enables continueOnFail for nodes with unconnected error outputs
|
|
162
|
+
*
|
|
163
|
+
* Handles L6_ERROR_OUTPUT_UNCONNECTED problems by:
|
|
164
|
+
* 1. Finding the node
|
|
165
|
+
* 2. Enabling continueOnFail to handle errors gracefully
|
|
166
|
+
*
|
|
167
|
+
* Confidence: 0.80 (HIGH - enabling error handling is safe)
|
|
168
|
+
* Auto-applicable: Yes - doesn't change workflow semantics significantly
|
|
169
|
+
*/
|
|
170
|
+
export class L6ErrorOutputFixer {
|
|
171
|
+
name = 'L6ErrorOutputFixer';
|
|
172
|
+
description = 'Enables continueOnFail for nodes with unconnected error outputs';
|
|
173
|
+
supportedCodes = ['L6_ERROR_OUTPUT_UNCONNECTED'];
|
|
174
|
+
minConfidence = 0.80;
|
|
175
|
+
canFix(problem) {
|
|
176
|
+
return problem.level === 'L6' && problem.code === 'L6_ERROR_OUTPUT_UNCONNECTED';
|
|
177
|
+
}
|
|
178
|
+
fix(workflow, problem) {
|
|
179
|
+
if (!this.canFix(problem)) {
|
|
180
|
+
return createFailureResult(this.name, problem.code, 'Problem does not meet requirements for L6ErrorOutputFixer');
|
|
181
|
+
}
|
|
182
|
+
// Try to find node by id/name, or fallback to extracting from description
|
|
183
|
+
const nodeNameFromDesc = extractNodeNameFromDescription(problem.description);
|
|
184
|
+
const node = findNode(workflow, problem.node_id, problem.node_name) ||
|
|
185
|
+
(nodeNameFromDesc ? findNode(workflow, undefined, nodeNameFromDesc) : undefined);
|
|
186
|
+
if (!node) {
|
|
187
|
+
return createFailureResult(this.name, problem.code, `Node not found: ${problem.node_id || problem.node_name || nodeNameFromDesc || 'unknown'}`);
|
|
188
|
+
}
|
|
189
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
190
|
+
const targetNode = findNode(fixedWorkflow, node.id, node.name);
|
|
191
|
+
// Store old values
|
|
192
|
+
const oldContinueOnFail = targetNode.continueOnFail;
|
|
193
|
+
const oldOnError = targetNode.onError;
|
|
194
|
+
// Enable error handling
|
|
195
|
+
targetNode.continueOnFail = true;
|
|
196
|
+
targetNode.onError = 'continueRegularOutput';
|
|
197
|
+
const change = {
|
|
198
|
+
change_type: 'parameter_update',
|
|
199
|
+
node_id: targetNode.id,
|
|
200
|
+
node_name: targetNode.name,
|
|
201
|
+
field: 'errorHandling',
|
|
202
|
+
old_value: {
|
|
203
|
+
continueOnFail: oldContinueOnFail,
|
|
204
|
+
onError: oldOnError,
|
|
205
|
+
},
|
|
206
|
+
new_value: {
|
|
207
|
+
continueOnFail: true,
|
|
208
|
+
onError: 'continueRegularOutput',
|
|
209
|
+
},
|
|
210
|
+
confidence: 0.80,
|
|
211
|
+
description: `Enabled continueOnFail for node "${targetNode.name}" to handle errors gracefully`,
|
|
212
|
+
};
|
|
213
|
+
logger.info('L6ErrorOutputFixer applied fix', {
|
|
214
|
+
node_name: targetNode.name,
|
|
215
|
+
problem_code: problem.code,
|
|
216
|
+
});
|
|
217
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], 0.80);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ============================================================
|
|
221
|
+
// L6 DEAD END FIXER
|
|
222
|
+
// ============================================================
|
|
223
|
+
/**
|
|
224
|
+
* L6DeadEndFixer - Handles dead end nodes
|
|
225
|
+
*
|
|
226
|
+
* Handles L6_DEAD_END_NODE problems by:
|
|
227
|
+
* 1. Finding the dead end node
|
|
228
|
+
* 2. Attempting to connect to an existing terminal node or set node
|
|
229
|
+
* 3. If no suitable target, marks node with noOp flag
|
|
230
|
+
*
|
|
231
|
+
* Confidence: 0.60 (MEDIUM - requires context to determine correct action)
|
|
232
|
+
* Auto-applicable: No - connection decisions require human judgment
|
|
233
|
+
*/
|
|
234
|
+
export class L6DeadEndFixer {
|
|
235
|
+
name = 'L6DeadEndFixer';
|
|
236
|
+
description = 'Handles dead end nodes by connecting or marking as intentional';
|
|
237
|
+
supportedCodes = ['L6_DEAD_END_NODE'];
|
|
238
|
+
minConfidence = 0.60;
|
|
239
|
+
canFix(problem) {
|
|
240
|
+
return problem.level === 'L6' && problem.code === 'L6_DEAD_END_NODE';
|
|
241
|
+
}
|
|
242
|
+
fix(workflow, problem) {
|
|
243
|
+
if (!this.canFix(problem)) {
|
|
244
|
+
return createFailureResult(this.name, problem.code, 'Problem does not meet requirements for L6DeadEndFixer');
|
|
245
|
+
}
|
|
246
|
+
// Try to find node by id/name, or fallback to extracting from description
|
|
247
|
+
const nodeNameFromDesc = extractNodeNameFromDescription(problem.description);
|
|
248
|
+
const node = findNode(workflow, problem.node_id, problem.node_name) ||
|
|
249
|
+
(nodeNameFromDesc ? findNode(workflow, undefined, nodeNameFromDesc) : undefined);
|
|
250
|
+
if (!node) {
|
|
251
|
+
return createFailureResult(this.name, problem.code, `Node not found: ${problem.node_id || problem.node_name || nodeNameFromDesc || 'unknown'}`);
|
|
252
|
+
}
|
|
253
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
254
|
+
// Strategy: Find the closest downstream node based on position
|
|
255
|
+
// and connect to it, or add a NoOp node
|
|
256
|
+
const targetNode = this.findBestTargetNode(workflow, node);
|
|
257
|
+
if (targetNode) {
|
|
258
|
+
// Connect to the found target
|
|
259
|
+
return this.connectToTarget(fixedWorkflow, node, targetNode, problem);
|
|
260
|
+
}
|
|
261
|
+
// No suitable target found - add noOp node as endpoint
|
|
262
|
+
return this.addNoOpEndpoint(fixedWorkflow, node, problem);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Find the best target node to connect to.
|
|
266
|
+
* Looks for Set, NoOp, or response nodes downstream.
|
|
267
|
+
*/
|
|
268
|
+
findBestTargetNode(workflow, sourceNode) {
|
|
269
|
+
const terminalTypes = [
|
|
270
|
+
'n8n-nodes-base.respondToWebhook',
|
|
271
|
+
'n8n-nodes-base.stopAndError',
|
|
272
|
+
'n8n-nodes-base.noOp',
|
|
273
|
+
'n8n-nodes-base.set',
|
|
274
|
+
];
|
|
275
|
+
// Find terminal nodes to the right of the source
|
|
276
|
+
const candidates = workflow.nodes.filter((n) => {
|
|
277
|
+
if (n.name === sourceNode.name)
|
|
278
|
+
return false;
|
|
279
|
+
if (n.position[0] <= sourceNode.position[0])
|
|
280
|
+
return false;
|
|
281
|
+
return terminalTypes.some((t) => n.type.toLowerCase().includes(t.toLowerCase().split('.')[1]));
|
|
282
|
+
});
|
|
283
|
+
if (candidates.length === 0)
|
|
284
|
+
return null;
|
|
285
|
+
// Return the closest one
|
|
286
|
+
return candidates.sort((a, b) => {
|
|
287
|
+
const distA = Math.abs(a.position[0] - sourceNode.position[0]) +
|
|
288
|
+
Math.abs(a.position[1] - sourceNode.position[1]);
|
|
289
|
+
const distB = Math.abs(b.position[0] - sourceNode.position[0]) +
|
|
290
|
+
Math.abs(b.position[1] - sourceNode.position[1]);
|
|
291
|
+
return distA - distB;
|
|
292
|
+
})[0];
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Connect source node to target node.
|
|
296
|
+
*/
|
|
297
|
+
connectToTarget(workflow, sourceNode, targetNode, problem) {
|
|
298
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
299
|
+
const sourceNodeName = sourceNode.name;
|
|
300
|
+
// Ensure connections object exists
|
|
301
|
+
if (!fixedWorkflow.connections[sourceNodeName]) {
|
|
302
|
+
fixedWorkflow.connections[sourceNodeName] = { main: [] };
|
|
303
|
+
}
|
|
304
|
+
if (!fixedWorkflow.connections[sourceNodeName].main) {
|
|
305
|
+
fixedWorkflow.connections[sourceNodeName].main = [];
|
|
306
|
+
}
|
|
307
|
+
// Add connection to first output
|
|
308
|
+
const outputs = fixedWorkflow.connections[sourceNodeName].main;
|
|
309
|
+
if (outputs.length === 0) {
|
|
310
|
+
outputs.push([]);
|
|
311
|
+
}
|
|
312
|
+
outputs[0].push({
|
|
313
|
+
node: targetNode.name,
|
|
314
|
+
type: 'main',
|
|
315
|
+
index: 0,
|
|
316
|
+
});
|
|
317
|
+
const change = {
|
|
318
|
+
change_type: 'parameter_update',
|
|
319
|
+
node_id: sourceNode.id,
|
|
320
|
+
node_name: sourceNode.name,
|
|
321
|
+
field: 'connections',
|
|
322
|
+
old_value: null,
|
|
323
|
+
new_value: { target: targetNode.name, type: 'main', index: 0 },
|
|
324
|
+
confidence: 0.60,
|
|
325
|
+
description: `Connected dead end "${sourceNode.name}" to existing node "${targetNode.name}"`,
|
|
326
|
+
};
|
|
327
|
+
logger.info('L6DeadEndFixer connected to existing node', {
|
|
328
|
+
source: sourceNode.name,
|
|
329
|
+
target: targetNode.name,
|
|
330
|
+
});
|
|
331
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], 0.60);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Add a NoOp node as endpoint and connect to it.
|
|
335
|
+
*/
|
|
336
|
+
addNoOpEndpoint(workflow, sourceNode, problem) {
|
|
337
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
338
|
+
// Create NoOp node
|
|
339
|
+
const noOpNode = {
|
|
340
|
+
id: generateNodeId(),
|
|
341
|
+
type: 'n8n-nodes-base.noOp',
|
|
342
|
+
name: `${sourceNode.name} End`,
|
|
343
|
+
position: [sourceNode.position[0] + 200, sourceNode.position[1]],
|
|
344
|
+
parameters: {},
|
|
345
|
+
typeVersion: 1,
|
|
346
|
+
};
|
|
347
|
+
// Add node to workflow
|
|
348
|
+
fixedWorkflow.nodes.push(noOpNode);
|
|
349
|
+
// Connect source to NoOp
|
|
350
|
+
if (!fixedWorkflow.connections[sourceNode.name]) {
|
|
351
|
+
fixedWorkflow.connections[sourceNode.name] = { main: [] };
|
|
352
|
+
}
|
|
353
|
+
if (!fixedWorkflow.connections[sourceNode.name].main) {
|
|
354
|
+
fixedWorkflow.connections[sourceNode.name].main = [];
|
|
355
|
+
}
|
|
356
|
+
const outputs = fixedWorkflow.connections[sourceNode.name].main;
|
|
357
|
+
if (outputs.length === 0) {
|
|
358
|
+
outputs.push([]);
|
|
359
|
+
}
|
|
360
|
+
outputs[0].push({
|
|
361
|
+
node: noOpNode.name,
|
|
362
|
+
type: 'main',
|
|
363
|
+
index: 0,
|
|
364
|
+
});
|
|
365
|
+
const change = {
|
|
366
|
+
change_type: 'parameter_update',
|
|
367
|
+
node_id: sourceNode.id,
|
|
368
|
+
node_name: sourceNode.name,
|
|
369
|
+
field: 'connections',
|
|
370
|
+
old_value: null,
|
|
371
|
+
new_value: { addedNode: noOpNode.name, type: 'noOp' },
|
|
372
|
+
confidence: 0.50,
|
|
373
|
+
description: `Added NoOp endpoint and connected dead end "${sourceNode.name}" to it`,
|
|
374
|
+
};
|
|
375
|
+
logger.info('L6DeadEndFixer added NoOp endpoint', {
|
|
376
|
+
source: sourceNode.name,
|
|
377
|
+
noOpNode: noOpNode.name,
|
|
378
|
+
});
|
|
379
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], 0.50);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// ============================================================
|
|
383
|
+
// L6 EXPRESSION FIXER
|
|
384
|
+
// ============================================================
|
|
385
|
+
/**
|
|
386
|
+
* L6ExpressionFixer - Fixes unbalanced braces in expressions
|
|
387
|
+
*
|
|
388
|
+
* Handles L6_UNBALANCED_BRACES and L6_INVALID_EXPRESSION_SYNTAX by:
|
|
389
|
+
* 1. Finding unbalanced {{ }} braces
|
|
390
|
+
* 2. Adding missing closing or opening braces
|
|
391
|
+
*
|
|
392
|
+
* Confidence: 0.85 (HIGH for simple brace balancing)
|
|
393
|
+
* Auto-applicable: Yes for simple cases
|
|
394
|
+
*/
|
|
395
|
+
export class L6ExpressionFixer {
|
|
396
|
+
name = 'L6ExpressionFixer';
|
|
397
|
+
description = 'Fixes unbalanced braces in expressions';
|
|
398
|
+
supportedCodes = ['L6_UNBALANCED_BRACES', 'L6_INVALID_EXPRESSION_SYNTAX'];
|
|
399
|
+
minConfidence = 0.85;
|
|
400
|
+
canFix(problem) {
|
|
401
|
+
if (problem.level !== 'L6')
|
|
402
|
+
return false;
|
|
403
|
+
return (problem.code === 'L6_UNBALANCED_BRACES' ||
|
|
404
|
+
problem.code === 'L6_INVALID_EXPRESSION_SYNTAX');
|
|
405
|
+
}
|
|
406
|
+
fix(workflow, problem) {
|
|
407
|
+
if (!this.canFix(problem)) {
|
|
408
|
+
return createFailureResult(this.name, problem.code, 'Problem does not meet requirements for L6ExpressionFixer');
|
|
409
|
+
}
|
|
410
|
+
// Try to find node by id/name, or fallback to extracting from description
|
|
411
|
+
const nodeNameFromDesc = extractNodeNameFromDescription(problem.description);
|
|
412
|
+
const node = findNode(workflow, problem.node_id, problem.node_name) ||
|
|
413
|
+
(nodeNameFromDesc ? findNode(workflow, undefined, nodeNameFromDesc) : undefined);
|
|
414
|
+
if (!node) {
|
|
415
|
+
return createFailureResult(this.name, problem.code, `Node not found: ${problem.node_id || problem.node_name || nodeNameFromDesc || 'unknown'}`);
|
|
416
|
+
}
|
|
417
|
+
// RAG-2.2.93.14: Get expression from raw_error (validation_problems table structure)
|
|
418
|
+
// Try multiple sources: raw_error.suggested_fix, raw_error.context, details, or description
|
|
419
|
+
const expressionFix = problem.raw_error?.suggested_fix?.patch?.expression_fix;
|
|
420
|
+
const expression = expressionFix?.old_value ||
|
|
421
|
+
problem.raw_error?.suggested_fix?.context?.expression_preview ||
|
|
422
|
+
problem.details?.expression ||
|
|
423
|
+
problem.details?.expression_preview;
|
|
424
|
+
if (!expression) {
|
|
425
|
+
// Log for debugging but flag for review instead of failing silently
|
|
426
|
+
logger.info('L6ExpressionFixer: No expression found, flagging for manual review', {
|
|
427
|
+
node_name: problem.node_name,
|
|
428
|
+
problem_code: problem.code,
|
|
429
|
+
has_raw_error: !!problem.raw_error,
|
|
430
|
+
has_suggested_fix: !!problem.raw_error?.suggested_fix,
|
|
431
|
+
});
|
|
432
|
+
return createFailureResult(this.name, problem.code, 'No expression found in raw_error or details - requires manual review');
|
|
433
|
+
}
|
|
434
|
+
// Get field path from raw_error if available
|
|
435
|
+
const fieldPath = expressionFix?.field_path ||
|
|
436
|
+
problem.path?.replace(/^nodes\.[^.]+\./, '') ||
|
|
437
|
+
'';
|
|
438
|
+
// Fix the expression
|
|
439
|
+
const fixedExpression = this.balanceBraces(expression);
|
|
440
|
+
if (fixedExpression === expression) {
|
|
441
|
+
// If we can't fix via brace balancing, check if raw_error has a suggested new_value
|
|
442
|
+
const suggestedFix = expressionFix?.new_value;
|
|
443
|
+
if (suggestedFix && suggestedFix !== expression) {
|
|
444
|
+
// Use the validator's suggested fix
|
|
445
|
+
return this.applyExpressionFix(workflow, node, expression, suggestedFix, fieldPath, problem);
|
|
446
|
+
}
|
|
447
|
+
logger.info('L6ExpressionFixer: Expression could not be auto-fixed', {
|
|
448
|
+
node_name: problem.node_name,
|
|
449
|
+
expression_preview: expression.substring(0, 50),
|
|
450
|
+
syntax_error: problem.raw_error?.suggested_fix?.context?.syntax_error,
|
|
451
|
+
});
|
|
452
|
+
return createFailureResult(this.name, problem.code, `Expression syntax error cannot be auto-fixed: ${problem.raw_error?.suggested_fix?.context?.syntax_error || 'unknown'}`);
|
|
453
|
+
}
|
|
454
|
+
return this.applyExpressionFix(workflow, node, expression, fixedExpression, fieldPath, problem);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Apply an expression fix to the workflow
|
|
458
|
+
*/
|
|
459
|
+
applyExpressionFix(workflow, node, oldExpression, newExpression, fieldPath, problem) {
|
|
460
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
461
|
+
const targetNode = findNode(fixedWorkflow, node.id, node.name);
|
|
462
|
+
// Apply fix to node parameters
|
|
463
|
+
if (fieldPath && targetNode.parameters) {
|
|
464
|
+
// Extract just the parameter path (remove 'nodes.NodeName.' prefix if present)
|
|
465
|
+
const paramPath = fieldPath
|
|
466
|
+
.replace(/^nodes\.[^.]+\./, '')
|
|
467
|
+
.replace(/^parameters\./, '');
|
|
468
|
+
this.setNestedValue(targetNode.parameters, paramPath, newExpression);
|
|
469
|
+
}
|
|
470
|
+
const change = {
|
|
471
|
+
change_type: 'parameter_update',
|
|
472
|
+
node_id: targetNode.id,
|
|
473
|
+
node_name: targetNode.name,
|
|
474
|
+
field: fieldPath || 'expression',
|
|
475
|
+
old_value: oldExpression,
|
|
476
|
+
new_value: newExpression,
|
|
477
|
+
confidence: 0.85,
|
|
478
|
+
description: `Fixed expression syntax: "${oldExpression.substring(0, 30)}..." → "${newExpression.substring(0, 30)}..."`,
|
|
479
|
+
};
|
|
480
|
+
logger.info('L6ExpressionFixer applied fix', {
|
|
481
|
+
node_name: targetNode.name,
|
|
482
|
+
original: oldExpression.substring(0, 50),
|
|
483
|
+
fixed: newExpression.substring(0, 50),
|
|
484
|
+
});
|
|
485
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], 0.85);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Balance {{ }} braces in an expression.
|
|
489
|
+
*/
|
|
490
|
+
balanceBraces(expr) {
|
|
491
|
+
let opens = 0;
|
|
492
|
+
let closes = 0;
|
|
493
|
+
let i = 0;
|
|
494
|
+
// Count {{ and }} pairs
|
|
495
|
+
while (i < expr.length - 1) {
|
|
496
|
+
if (expr[i] === '{' && expr[i + 1] === '{') {
|
|
497
|
+
opens++;
|
|
498
|
+
i += 2;
|
|
499
|
+
}
|
|
500
|
+
else if (expr[i] === '}' && expr[i + 1] === '}') {
|
|
501
|
+
closes++;
|
|
502
|
+
i += 2;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
i++;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Balance the braces
|
|
509
|
+
if (opens > closes) {
|
|
510
|
+
return expr + '}}'.repeat(opens - closes);
|
|
511
|
+
}
|
|
512
|
+
else if (closes > opens) {
|
|
513
|
+
return '{{'.repeat(closes - opens) + expr;
|
|
514
|
+
}
|
|
515
|
+
return expr;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Set a nested value in an object using dot notation path.
|
|
519
|
+
*/
|
|
520
|
+
setNestedValue(obj, path, value) {
|
|
521
|
+
const parts = path.split('.');
|
|
522
|
+
let current = obj;
|
|
523
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
524
|
+
const part = parts[i];
|
|
525
|
+
// Handle array indexing like [0]
|
|
526
|
+
const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
|
|
527
|
+
if (arrayMatch) {
|
|
528
|
+
const [, key, index] = arrayMatch;
|
|
529
|
+
if (!current[key])
|
|
530
|
+
current[key] = [];
|
|
531
|
+
if (!current[key][parseInt(index)])
|
|
532
|
+
current[key][parseInt(index)] = {};
|
|
533
|
+
current = current[key][parseInt(index)];
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
if (!current[part])
|
|
537
|
+
current[part] = {};
|
|
538
|
+
current = current[part];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const lastPart = parts[parts.length - 1];
|
|
542
|
+
const lastArrayMatch = lastPart.match(/^(.+)\[(\d+)\]$/);
|
|
543
|
+
if (lastArrayMatch) {
|
|
544
|
+
const [, key, index] = lastArrayMatch;
|
|
545
|
+
if (!current[key])
|
|
546
|
+
current[key] = [];
|
|
547
|
+
current[key][parseInt(index)] = value;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
current[lastPart] = value;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// ============================================================
|
|
555
|
+
// L6 TRIGGER FIXER
|
|
556
|
+
// ============================================================
|
|
557
|
+
/**
|
|
558
|
+
* L6TriggerFixer - Adds manual trigger when missing
|
|
559
|
+
*
|
|
560
|
+
* Handles L6_NO_TRIGGER problems by:
|
|
561
|
+
* 1. Adding a Manual Trigger node
|
|
562
|
+
* 2. Connecting it to the first non-trigger node
|
|
563
|
+
*
|
|
564
|
+
* Confidence: 0.90 (HIGH - manual trigger is safe default)
|
|
565
|
+
* Auto-applicable: Yes - adding trigger allows workflow execution
|
|
566
|
+
*/
|
|
567
|
+
export class L6TriggerFixer {
|
|
568
|
+
name = 'L6TriggerFixer';
|
|
569
|
+
description = 'Adds manual trigger when workflow has no trigger';
|
|
570
|
+
supportedCodes = ['L6_NO_TRIGGER'];
|
|
571
|
+
minConfidence = 0.90;
|
|
572
|
+
canFix(problem) {
|
|
573
|
+
return problem.level === 'L6' && problem.code === 'L6_NO_TRIGGER';
|
|
574
|
+
}
|
|
575
|
+
fix(workflow, problem) {
|
|
576
|
+
if (!this.canFix(problem)) {
|
|
577
|
+
return createFailureResult(this.name, problem.code, 'Problem does not meet requirements for L6TriggerFixer');
|
|
578
|
+
}
|
|
579
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
580
|
+
// Create Manual Trigger node
|
|
581
|
+
const triggerNode = {
|
|
582
|
+
id: generateNodeId(),
|
|
583
|
+
type: 'n8n-nodes-base.manualTrigger',
|
|
584
|
+
name: 'Manual Trigger',
|
|
585
|
+
position: this.calculateTriggerPosition(workflow),
|
|
586
|
+
parameters: {},
|
|
587
|
+
typeVersion: 1,
|
|
588
|
+
};
|
|
589
|
+
// Add trigger to workflow
|
|
590
|
+
fixedWorkflow.nodes.push(triggerNode);
|
|
591
|
+
// Find the first node to connect to (leftmost non-trigger)
|
|
592
|
+
const firstNode = this.findFirstNode(workflow);
|
|
593
|
+
if (firstNode) {
|
|
594
|
+
// Connect trigger to first node
|
|
595
|
+
fixedWorkflow.connections[triggerNode.name] = {
|
|
596
|
+
main: [[{ node: firstNode.name, type: 'main', index: 0 }]],
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const change = {
|
|
600
|
+
change_type: 'parameter_update',
|
|
601
|
+
node_id: triggerNode.id,
|
|
602
|
+
node_name: triggerNode.name,
|
|
603
|
+
field: 'nodes',
|
|
604
|
+
old_value: null,
|
|
605
|
+
new_value: {
|
|
606
|
+
addedTrigger: triggerNode.name,
|
|
607
|
+
connectedTo: firstNode?.name || null,
|
|
608
|
+
},
|
|
609
|
+
confidence: 0.90,
|
|
610
|
+
description: `Added Manual Trigger and connected to "${firstNode?.name || 'workflow'}"`,
|
|
611
|
+
};
|
|
612
|
+
logger.info('L6TriggerFixer applied fix', {
|
|
613
|
+
trigger: triggerNode.name,
|
|
614
|
+
connectedTo: firstNode?.name,
|
|
615
|
+
});
|
|
616
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], 0.90);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Calculate position for new trigger node (left of all existing nodes).
|
|
620
|
+
*/
|
|
621
|
+
calculateTriggerPosition(workflow) {
|
|
622
|
+
if (workflow.nodes.length === 0) {
|
|
623
|
+
return [250, 300];
|
|
624
|
+
}
|
|
625
|
+
const minX = Math.min(...workflow.nodes.map((n) => n.position[0]));
|
|
626
|
+
const avgY = workflow.nodes.reduce((sum, n) => sum + n.position[1], 0) / workflow.nodes.length;
|
|
627
|
+
return [minX - 250, avgY];
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Find the first node in the workflow (leftmost).
|
|
631
|
+
*/
|
|
632
|
+
findFirstNode(workflow) {
|
|
633
|
+
if (workflow.nodes.length === 0)
|
|
634
|
+
return null;
|
|
635
|
+
return workflow.nodes.reduce((leftmost, node) => {
|
|
636
|
+
if (node.position[0] < leftmost.position[0]) {
|
|
637
|
+
return node;
|
|
638
|
+
}
|
|
639
|
+
return leftmost;
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// ============================================================
|
|
644
|
+
// L6 HARDCODED URL GAP RECORDER
|
|
645
|
+
// ============================================================
|
|
646
|
+
/**
|
|
647
|
+
* L6HardcodedUrlHandler - Records hardcoded URLs as gaps (not auto-fixable)
|
|
648
|
+
*
|
|
649
|
+
* Handles L6_HARDCODED_URL by recording to factory.gaps table.
|
|
650
|
+
* These are NOT auto-fixable because:
|
|
651
|
+
* 1. URLs may be intentionally hardcoded for specific environments
|
|
652
|
+
* 2. Replacing requires knowledge of target environment
|
|
653
|
+
* 3. Security decision requires human review
|
|
654
|
+
*
|
|
655
|
+
* This "fixer" returns success: false but records the gap.
|
|
656
|
+
*/
|
|
657
|
+
export class L6HardcodedUrlHandler {
|
|
658
|
+
name = 'L6HardcodedUrlHandler';
|
|
659
|
+
description = 'Records hardcoded URLs as gaps (not auto-fixable)';
|
|
660
|
+
supportedCodes = ['L6_HARDCODED_URL'];
|
|
661
|
+
minConfidence = 0.0; // Not auto-applicable
|
|
662
|
+
canFix(problem) {
|
|
663
|
+
return problem.level === 'L6' && problem.code === 'L6_HARDCODED_URL';
|
|
664
|
+
}
|
|
665
|
+
fix(workflow, problem) {
|
|
666
|
+
// This handler records the gap but doesn't actually fix anything
|
|
667
|
+
// The gap recording happens in the pipeline, not here
|
|
668
|
+
logger.info('L6HardcodedUrlHandler: Gap recorded (not auto-fixable)', {
|
|
669
|
+
workflow_id: workflow.id,
|
|
670
|
+
node_name: problem.node_name,
|
|
671
|
+
code: problem.code,
|
|
672
|
+
});
|
|
673
|
+
return {
|
|
674
|
+
success: false,
|
|
675
|
+
changes: [],
|
|
676
|
+
reason: 'HARDCODED_URL is not auto-fixable - recorded as gap for manual review',
|
|
677
|
+
confidence: 0,
|
|
678
|
+
fixer_name: this.name,
|
|
679
|
+
problem_code: problem.code,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// ============================================================
|
|
684
|
+
// L6 UNREACHABLE NODE FIXER v2 (RAG-2.2.108.1)
|
|
685
|
+
// ============================================================
|
|
686
|
+
/**
|
|
687
|
+
* Confidence thresholds for position-based connection algorithm
|
|
688
|
+
* RAG-2.2.108.1: Named constants for position-based confidence tiers
|
|
689
|
+
*/
|
|
690
|
+
const UNREACHABLE_FIXER_CONFIG = {
|
|
691
|
+
/** Y-distance threshold for same-row nodes (pixels) */
|
|
692
|
+
SAME_ROW_THRESHOLD: 50,
|
|
693
|
+
/** Y-distance threshold for close-row nodes (pixels) */
|
|
694
|
+
CLOSE_ROW_THRESHOLD: 150,
|
|
695
|
+
/** Confidence for same-row connections */
|
|
696
|
+
SAME_ROW_CONFIDENCE: 0.90,
|
|
697
|
+
/** Confidence for close-row connections */
|
|
698
|
+
CLOSE_ROW_CONFIDENCE: 0.75,
|
|
699
|
+
/** Confidence for distant nodes (manual review) */
|
|
700
|
+
DISTANT_CONFIDENCE: 0.40,
|
|
701
|
+
/** Confidence for disabled node skip */
|
|
702
|
+
DISABLED_NODE_CONFIDENCE: 1.0,
|
|
703
|
+
/** Confidence for duplicate trigger detection */
|
|
704
|
+
DUPLICATE_TRIGGER_CONFIDENCE: 0.90,
|
|
705
|
+
};
|
|
706
|
+
/**
|
|
707
|
+
* Known trigger node types for duplicate trigger detection
|
|
708
|
+
* RAG-2.2.108.1: List of node types that act as workflow entry points
|
|
709
|
+
*/
|
|
710
|
+
const TRIGGER_NODE_TYPES = new Set([
|
|
711
|
+
'n8n-nodes-base.webhook',
|
|
712
|
+
'n8n-nodes-base.manualTrigger',
|
|
713
|
+
'n8n-nodes-base.scheduleTrigger',
|
|
714
|
+
'n8n-nodes-base.cron',
|
|
715
|
+
'n8n-nodes-base.emailReadImap',
|
|
716
|
+
'n8n-nodes-base.emailTrigger',
|
|
717
|
+
'n8n-nodes-base.telegramTrigger',
|
|
718
|
+
'n8n-nodes-base.slackTrigger',
|
|
719
|
+
'n8n-nodes-base.githubTrigger',
|
|
720
|
+
'n8n-nodes-base.gitlabTrigger',
|
|
721
|
+
'n8n-nodes-base.stripeTrigger',
|
|
722
|
+
'n8n-nodes-base.shopifyTrigger',
|
|
723
|
+
'n8n-nodes-base.airtableTrigger',
|
|
724
|
+
'n8n-nodes-base.googleSheetsTrigger',
|
|
725
|
+
'n8n-nodes-base.formTrigger',
|
|
726
|
+
'n8n-nodes-base.executeWorkflowTrigger',
|
|
727
|
+
]);
|
|
728
|
+
/**
|
|
729
|
+
* Check if a node type is a trigger type
|
|
730
|
+
*/
|
|
731
|
+
function isTriggerType(nodeType) {
|
|
732
|
+
const lowerType = nodeType.toLowerCase();
|
|
733
|
+
// Check exact match
|
|
734
|
+
if (TRIGGER_NODE_TYPES.has(lowerType))
|
|
735
|
+
return true;
|
|
736
|
+
// Check if ends with 'trigger' (catches custom/community triggers)
|
|
737
|
+
return lowerType.endsWith('trigger');
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Calculate the Y-distance between two nodes
|
|
741
|
+
*/
|
|
742
|
+
function calculateYDistance(node1, node2) {
|
|
743
|
+
return Math.abs(node1.position[1] - node2.position[1]);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Find all upstream nodes (nodes to the left that could potentially connect)
|
|
747
|
+
*/
|
|
748
|
+
function findUpstreamNodes(workflow, targetNode) {
|
|
749
|
+
return workflow.nodes.filter((node) => {
|
|
750
|
+
// Must be to the left of target
|
|
751
|
+
if (node.position[0] >= targetNode.position[0])
|
|
752
|
+
return false;
|
|
753
|
+
// Must not be the same node
|
|
754
|
+
if (node.id === targetNode.id || node.name === targetNode.name)
|
|
755
|
+
return false;
|
|
756
|
+
// Exclude sticky notes and other non-flow nodes
|
|
757
|
+
const lowerType = node.type.toLowerCase();
|
|
758
|
+
if (lowerType.includes('stickynote') || lowerType.includes('noop'))
|
|
759
|
+
return false;
|
|
760
|
+
return true;
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Find the closest upstream node by Y-distance
|
|
765
|
+
*/
|
|
766
|
+
function findClosestUpstreamByY(upstreamNodes, targetNode) {
|
|
767
|
+
if (upstreamNodes.length === 0)
|
|
768
|
+
return null;
|
|
769
|
+
let closest = upstreamNodes[0];
|
|
770
|
+
let minYDistance = calculateYDistance(closest, targetNode);
|
|
771
|
+
for (const node of upstreamNodes) {
|
|
772
|
+
const yDistance = calculateYDistance(node, targetNode);
|
|
773
|
+
if (yDistance < minYDistance) {
|
|
774
|
+
minYDistance = yDistance;
|
|
775
|
+
closest = node;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return { node: closest, yDistance: minYDistance };
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Determine confidence tier based on Y-distance
|
|
782
|
+
*/
|
|
783
|
+
function getConfidenceTier(yDistance) {
|
|
784
|
+
if (yDistance < UNREACHABLE_FIXER_CONFIG.SAME_ROW_THRESHOLD) {
|
|
785
|
+
return { confidence: UNREACHABLE_FIXER_CONFIG.SAME_ROW_CONFIDENCE, tier: 'same-row' };
|
|
786
|
+
}
|
|
787
|
+
if (yDistance < UNREACHABLE_FIXER_CONFIG.CLOSE_ROW_THRESHOLD) {
|
|
788
|
+
return { confidence: UNREACHABLE_FIXER_CONFIG.CLOSE_ROW_CONFIDENCE, tier: 'close-row' };
|
|
789
|
+
}
|
|
790
|
+
return { confidence: UNREACHABLE_FIXER_CONFIG.DISTANT_CONFIDENCE, tier: 'distant' };
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* L6UnreachableNodeFixerV2 - Position-based unreachable node fixer
|
|
794
|
+
*
|
|
795
|
+
* RAG-2.2.108.1: Replaces the always-fail handler with intelligent fixes:
|
|
796
|
+
* 1. Skip disabled nodes (confidence 1.0, not an error)
|
|
797
|
+
* 2. Flag duplicate triggers (confidence 0.90)
|
|
798
|
+
* 3. Same-row connection (Y-distance <50px → confidence 0.90)
|
|
799
|
+
* 4. Close-row connection (Y-distance <150px → confidence 0.75)
|
|
800
|
+
* 5. Distant nodes flag for manual review (confidence 0.40)
|
|
801
|
+
*
|
|
802
|
+
* Expected Impact: ~77% of unreachable nodes auto-fixed (same-row instances)
|
|
803
|
+
*/
|
|
804
|
+
export class L6UnreachableNodeFixerV2 {
|
|
805
|
+
name = 'L6UnreachableNodeFixerV2';
|
|
806
|
+
description = 'Position-based unreachable node fixer with confidence tiers';
|
|
807
|
+
supportedCodes = ['L6_UNREACHABLE_NODE'];
|
|
808
|
+
minConfidence = 0.40;
|
|
809
|
+
canFix(problem) {
|
|
810
|
+
return problem.level === 'L6' && problem.code === 'L6_UNREACHABLE_NODE';
|
|
811
|
+
}
|
|
812
|
+
fix(workflow, problem) {
|
|
813
|
+
if (!this.canFix(problem)) {
|
|
814
|
+
return createFailureResult(this.name, problem.code, 'Problem does not meet requirements for L6UnreachableNodeFixerV2');
|
|
815
|
+
}
|
|
816
|
+
// Find the unreachable node
|
|
817
|
+
const nodeNameFromDesc = extractNodeNameFromDescription(problem.description);
|
|
818
|
+
const node = findNode(workflow, problem.node_id, problem.node_name) ||
|
|
819
|
+
(nodeNameFromDesc ? findNode(workflow, undefined, nodeNameFromDesc) : undefined);
|
|
820
|
+
if (!node) {
|
|
821
|
+
return createFailureResult(this.name, problem.code, `Node not found: ${problem.node_id || problem.node_name || nodeNameFromDesc || 'unknown'}`);
|
|
822
|
+
}
|
|
823
|
+
// Step 1: Check if node is disabled → SKIP (confidence 1.0, not an error)
|
|
824
|
+
if (this.isNodeDisabled(node)) {
|
|
825
|
+
return this.createSkipDisabledResult(node, problem);
|
|
826
|
+
}
|
|
827
|
+
// Step 2: Check if node is a trigger type
|
|
828
|
+
if (isTriggerType(node.type)) {
|
|
829
|
+
return this.handleTriggerNode(workflow, node, problem);
|
|
830
|
+
}
|
|
831
|
+
// Step 3-7: Position-based connection algorithm
|
|
832
|
+
return this.applyPositionBasedFix(workflow, node, problem);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Check if a node is disabled
|
|
836
|
+
*/
|
|
837
|
+
isNodeDisabled(node) {
|
|
838
|
+
return node.disabled === true;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Create result for disabled node skip
|
|
842
|
+
*/
|
|
843
|
+
createSkipDisabledResult(node, problem) {
|
|
844
|
+
logger.info('L6UnreachableNodeFixerV2: Skipping disabled node', {
|
|
845
|
+
node_name: node.name,
|
|
846
|
+
node_type: node.type,
|
|
847
|
+
});
|
|
848
|
+
// Disabled nodes should be skipped entirely - not an error
|
|
849
|
+
// Return success with confidence 1.0 and no changes needed
|
|
850
|
+
return {
|
|
851
|
+
success: true,
|
|
852
|
+
fixedWorkflow: undefined, // No workflow changes needed
|
|
853
|
+
changes: [{
|
|
854
|
+
change_type: 'parameter_update',
|
|
855
|
+
node_id: node.id,
|
|
856
|
+
node_name: node.name,
|
|
857
|
+
field: 'status',
|
|
858
|
+
old_value: 'unreachable',
|
|
859
|
+
new_value: 'disabled_skip',
|
|
860
|
+
confidence: UNREACHABLE_FIXER_CONFIG.DISABLED_NODE_CONFIDENCE,
|
|
861
|
+
description: `Skipped disabled node "${node.name}" - not an error`,
|
|
862
|
+
}],
|
|
863
|
+
confidence: UNREACHABLE_FIXER_CONFIG.DISABLED_NODE_CONFIDENCE,
|
|
864
|
+
fixer_name: this.name,
|
|
865
|
+
problem_code: problem.code,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Handle trigger-type nodes
|
|
870
|
+
*/
|
|
871
|
+
handleTriggerNode(workflow, node, problem) {
|
|
872
|
+
// Count existing triggers in the workflow
|
|
873
|
+
const triggers = workflow.nodes.filter((n) => isTriggerType(n.type));
|
|
874
|
+
const otherTriggers = triggers.filter((t) => t.id !== node.id && t.name !== node.name);
|
|
875
|
+
if (otherTriggers.length > 0) {
|
|
876
|
+
// Another trigger exists → flag as duplicate trigger for removal
|
|
877
|
+
logger.info('L6UnreachableNodeFixerV2: Detected duplicate trigger', {
|
|
878
|
+
node_name: node.name,
|
|
879
|
+
node_type: node.type,
|
|
880
|
+
other_triggers: otherTriggers.map((t) => t.name),
|
|
881
|
+
});
|
|
882
|
+
return {
|
|
883
|
+
success: true,
|
|
884
|
+
fixedWorkflow: undefined, // No auto-removal, just flagged
|
|
885
|
+
changes: [{
|
|
886
|
+
change_type: 'parameter_update',
|
|
887
|
+
node_id: node.id,
|
|
888
|
+
node_name: node.name,
|
|
889
|
+
field: 'status',
|
|
890
|
+
old_value: 'unreachable',
|
|
891
|
+
new_value: 'duplicate_trigger',
|
|
892
|
+
confidence: UNREACHABLE_FIXER_CONFIG.DUPLICATE_TRIGGER_CONFIDENCE,
|
|
893
|
+
description: `Duplicate trigger "${node.name}" - consider removal (primary: ${otherTriggers[0].name})`,
|
|
894
|
+
}],
|
|
895
|
+
confidence: UNREACHABLE_FIXER_CONFIG.DUPLICATE_TRIGGER_CONFIDENCE,
|
|
896
|
+
fixer_name: this.name,
|
|
897
|
+
problem_code: problem.code,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
// Only trigger in workflow but unreachable - workflow has no entry point
|
|
901
|
+
logger.info('L6UnreachableNodeFixerV2: Orphan trigger (no entry point)', {
|
|
902
|
+
node_name: node.name,
|
|
903
|
+
});
|
|
904
|
+
return createFailureResult(this.name, problem.code, `Trigger "${node.name}" is unreachable and is the only trigger - workflow may have structural issues`);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Apply position-based fix algorithm
|
|
908
|
+
*/
|
|
909
|
+
applyPositionBasedFix(workflow, node, problem) {
|
|
910
|
+
// Find all upstream nodes (to the left)
|
|
911
|
+
const upstreamNodes = findUpstreamNodes(workflow, node);
|
|
912
|
+
if (upstreamNodes.length === 0) {
|
|
913
|
+
// No upstream nodes available
|
|
914
|
+
logger.info('L6UnreachableNodeFixerV2: No upstream nodes found', {
|
|
915
|
+
node_name: node.name,
|
|
916
|
+
});
|
|
917
|
+
return createFailureResult(this.name, problem.code, `No upstream nodes found for "${node.name}" - may be orphaned or misplaced`);
|
|
918
|
+
}
|
|
919
|
+
// Find closest upstream by Y-distance
|
|
920
|
+
const closest = findClosestUpstreamByY(upstreamNodes, node);
|
|
921
|
+
if (!closest) {
|
|
922
|
+
return createFailureResult(this.name, problem.code, `Could not determine closest upstream for "${node.name}"`);
|
|
923
|
+
}
|
|
924
|
+
// Determine confidence tier
|
|
925
|
+
const { confidence, tier } = getConfidenceTier(closest.yDistance);
|
|
926
|
+
// If distant, flag for manual review
|
|
927
|
+
if (tier === 'distant') {
|
|
928
|
+
logger.info('L6UnreachableNodeFixerV2: Distant node flagged for review', {
|
|
929
|
+
node_name: node.name,
|
|
930
|
+
closest_upstream: closest.node.name,
|
|
931
|
+
y_distance: closest.yDistance,
|
|
932
|
+
confidence,
|
|
933
|
+
});
|
|
934
|
+
return {
|
|
935
|
+
success: false,
|
|
936
|
+
changes: [],
|
|
937
|
+
reason: `Unreachable node "${node.name}" is distant from closest upstream "${closest.node.name}" (Y-distance: ${closest.yDistance}px) - requires manual review`,
|
|
938
|
+
confidence,
|
|
939
|
+
fixer_name: this.name,
|
|
940
|
+
problem_code: problem.code,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
// Same-row or close-row: apply connection fix
|
|
944
|
+
return this.createConnectionFix(workflow, node, closest.node, confidence, tier, problem);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Create connection fix between upstream and target node
|
|
948
|
+
*/
|
|
949
|
+
createConnectionFix(workflow, targetNode, upstreamNode, confidence, tier, problem) {
|
|
950
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
951
|
+
// Ensure connections object exists for upstream node
|
|
952
|
+
if (!fixedWorkflow.connections[upstreamNode.name]) {
|
|
953
|
+
fixedWorkflow.connections[upstreamNode.name] = { main: [] };
|
|
954
|
+
}
|
|
955
|
+
if (!fixedWorkflow.connections[upstreamNode.name].main) {
|
|
956
|
+
fixedWorkflow.connections[upstreamNode.name].main = [];
|
|
957
|
+
}
|
|
958
|
+
// Add connection from upstream to target
|
|
959
|
+
const outputs = fixedWorkflow.connections[upstreamNode.name].main;
|
|
960
|
+
if (outputs.length === 0) {
|
|
961
|
+
outputs.push([]);
|
|
962
|
+
}
|
|
963
|
+
// Add to first available output
|
|
964
|
+
outputs[0].push({
|
|
965
|
+
node: targetNode.name,
|
|
966
|
+
type: 'main',
|
|
967
|
+
index: 0,
|
|
968
|
+
});
|
|
969
|
+
const yDistance = calculateYDistance(upstreamNode, targetNode);
|
|
970
|
+
const change = {
|
|
971
|
+
change_type: 'parameter_update',
|
|
972
|
+
node_id: targetNode.id,
|
|
973
|
+
node_name: targetNode.name,
|
|
974
|
+
field: 'connections',
|
|
975
|
+
old_value: null,
|
|
976
|
+
new_value: {
|
|
977
|
+
source: upstreamNode.name,
|
|
978
|
+
target: targetNode.name,
|
|
979
|
+
type: 'main',
|
|
980
|
+
index: 0,
|
|
981
|
+
},
|
|
982
|
+
confidence,
|
|
983
|
+
description: `Connected ${tier} node "${targetNode.name}" to upstream "${upstreamNode.name}" (Y-distance: ${yDistance}px)`,
|
|
984
|
+
};
|
|
985
|
+
logger.info('L6UnreachableNodeFixerV2: Applied connection fix', {
|
|
986
|
+
target: targetNode.name,
|
|
987
|
+
upstream: upstreamNode.name,
|
|
988
|
+
tier,
|
|
989
|
+
y_distance: yDistance,
|
|
990
|
+
confidence,
|
|
991
|
+
});
|
|
992
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], confidence);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Keep legacy handler for backward compatibility (deprecated)
|
|
996
|
+
/**
|
|
997
|
+
* @deprecated Use L6UnreachableNodeFixerV2 instead
|
|
998
|
+
* L6UnreachableNodeHandler - Legacy handler that flags for manual review
|
|
999
|
+
*/
|
|
1000
|
+
export class L6UnreachableNodeHandler {
|
|
1001
|
+
name = 'L6UnreachableNodeHandler';
|
|
1002
|
+
description = '[DEPRECATED] Use L6UnreachableNodeFixerV2 - Flags unreachable nodes for manual review';
|
|
1003
|
+
supportedCodes = ['L6_UNREACHABLE_NODE'];
|
|
1004
|
+
minConfidence = 0.40;
|
|
1005
|
+
canFix(problem) {
|
|
1006
|
+
return problem.level === 'L6' && problem.code === 'L6_UNREACHABLE_NODE';
|
|
1007
|
+
}
|
|
1008
|
+
fix(workflow, problem) {
|
|
1009
|
+
// Delegate to V2 fixer
|
|
1010
|
+
const v2Fixer = new L6UnreachableNodeFixerV2();
|
|
1011
|
+
return v2Fixer.fix(workflow, problem);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// ============================================================
|
|
1015
|
+
// L6 DEAD END FIXER V2 (RAG-2.2.108.2)
|
|
1016
|
+
// ============================================================
|
|
1017
|
+
/**
|
|
1018
|
+
* Configuration for dead-end fixer v2
|
|
1019
|
+
* RAG-2.2.108.2: Named constants for terminal detection and confidence tiers
|
|
1020
|
+
*/
|
|
1021
|
+
const DEAD_END_FIXER_CONFIG = {
|
|
1022
|
+
/** Confidence for valid terminal node skip */
|
|
1023
|
+
VALID_TERMINAL_CONFIDENCE: 1.0,
|
|
1024
|
+
/** Confidence for single downstream auto-connection */
|
|
1025
|
+
SINGLE_DOWNSTREAM_CONFIDENCE: 0.90,
|
|
1026
|
+
/** Confidence for closest-Y downstream with multiple targets */
|
|
1027
|
+
MULTIPLE_DOWNSTREAM_CONFIDENCE: 0.75,
|
|
1028
|
+
/** Confidence for NoOp addition on data transform dead-ends */
|
|
1029
|
+
DATA_TRANSFORM_NOOP_CONFIDENCE: 0.85,
|
|
1030
|
+
/** Confidence for flow control missing branch flag */
|
|
1031
|
+
FLOW_CONTROL_FLAG_CONFIDENCE: 0.50,
|
|
1032
|
+
/** Confidence for manual review flag */
|
|
1033
|
+
MANUAL_REVIEW_CONFIDENCE: 0.50,
|
|
1034
|
+
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Valid terminal node types that should NOT be flagged as dead-ends
|
|
1037
|
+
* RAG-2.2.108.2: These nodes are intentional endpoints
|
|
1038
|
+
*/
|
|
1039
|
+
const VALID_TERMINAL_TYPES = new Set([
|
|
1040
|
+
// Always terminal - explicit workflow endpoints
|
|
1041
|
+
'n8n-nodes-base.respondToWebhook',
|
|
1042
|
+
'n8n-nodes-base.stopAndError',
|
|
1043
|
+
'n8n-nodes-base.noOp',
|
|
1044
|
+
// Messaging - final action nodes (send and done)
|
|
1045
|
+
'n8n-nodes-base.slack',
|
|
1046
|
+
'n8n-nodes-base.telegram',
|
|
1047
|
+
'n8n-nodes-base.whatsApp',
|
|
1048
|
+
'n8n-nodes-base.discord',
|
|
1049
|
+
'n8n-nodes-base.microsoftTeams',
|
|
1050
|
+
// AI response nodes
|
|
1051
|
+
'@n8n/n8n-nodes-langchain.openAi',
|
|
1052
|
+
'@n8n/n8n-nodes-langchain.chat',
|
|
1053
|
+
'@n8n/n8n-nodes-langchain.agent',
|
|
1054
|
+
'@n8n/n8n-nodes-langchain.chainLlm',
|
|
1055
|
+
// Issue tracking / notification - final action nodes
|
|
1056
|
+
'n8n-nodes-base.linear',
|
|
1057
|
+
'n8n-nodes-base.zendesk',
|
|
1058
|
+
'n8n-nodes-base.jira',
|
|
1059
|
+
'n8n-nodes-base.emailSend',
|
|
1060
|
+
'n8n-nodes-base.gmail',
|
|
1061
|
+
// Database write operations (commonly terminal)
|
|
1062
|
+
'n8n-nodes-base.postgres',
|
|
1063
|
+
'n8n-nodes-base.mysql',
|
|
1064
|
+
'n8n-nodes-base.mongoDb',
|
|
1065
|
+
'n8n-nodes-base.redis',
|
|
1066
|
+
// Webhook response
|
|
1067
|
+
'n8n-nodes-base.respondToWebhook',
|
|
1068
|
+
]);
|
|
1069
|
+
/**
|
|
1070
|
+
* Data transform node types that should get NoOp appended
|
|
1071
|
+
* RAG-2.2.108.2: These nodes need a downstream to not lose data
|
|
1072
|
+
*/
|
|
1073
|
+
const DATA_TRANSFORM_TYPES = new Set([
|
|
1074
|
+
'n8n-nodes-base.set',
|
|
1075
|
+
'n8n-nodes-base.code',
|
|
1076
|
+
'n8n-nodes-base.functionItem',
|
|
1077
|
+
'n8n-nodes-base.function',
|
|
1078
|
+
'n8n-nodes-base.itemLists',
|
|
1079
|
+
]);
|
|
1080
|
+
/**
|
|
1081
|
+
* Flow control node types that may have missing branches
|
|
1082
|
+
* RAG-2.2.108.2: These nodes need all branches connected
|
|
1083
|
+
*/
|
|
1084
|
+
const FLOW_CONTROL_TYPES = new Set([
|
|
1085
|
+
'n8n-nodes-base.if',
|
|
1086
|
+
'n8n-nodes-base.switch',
|
|
1087
|
+
'n8n-nodes-base.filter',
|
|
1088
|
+
]);
|
|
1089
|
+
/**
|
|
1090
|
+
* Check if a node type is a valid terminal type
|
|
1091
|
+
*/
|
|
1092
|
+
function isValidTerminalType(nodeType) {
|
|
1093
|
+
const lowerType = nodeType.toLowerCase();
|
|
1094
|
+
// Check exact match
|
|
1095
|
+
if (VALID_TERMINAL_TYPES.has(nodeType))
|
|
1096
|
+
return true;
|
|
1097
|
+
// Check lowercase match
|
|
1098
|
+
for (const terminalType of VALID_TERMINAL_TYPES) {
|
|
1099
|
+
if (lowerType === terminalType.toLowerCase())
|
|
1100
|
+
return true;
|
|
1101
|
+
// Check partial match for base type (e.g., 'slack' in 'n8n-nodes-base.slack')
|
|
1102
|
+
const baseName = terminalType.split('.').pop()?.toLowerCase();
|
|
1103
|
+
if (baseName && lowerType.includes(baseName))
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Check if a node type is a data transform type
|
|
1110
|
+
*/
|
|
1111
|
+
function isDataTransformType(nodeType) {
|
|
1112
|
+
const lowerType = nodeType.toLowerCase();
|
|
1113
|
+
for (const transformType of DATA_TRANSFORM_TYPES) {
|
|
1114
|
+
if (lowerType === transformType.toLowerCase())
|
|
1115
|
+
return true;
|
|
1116
|
+
const baseName = transformType.split('.').pop()?.toLowerCase();
|
|
1117
|
+
if (baseName && lowerType.includes(baseName))
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Check if a node type is a flow control type
|
|
1124
|
+
*/
|
|
1125
|
+
function isFlowControlType(nodeType) {
|
|
1126
|
+
const lowerType = nodeType.toLowerCase();
|
|
1127
|
+
for (const controlType of FLOW_CONTROL_TYPES) {
|
|
1128
|
+
if (lowerType === controlType.toLowerCase())
|
|
1129
|
+
return true;
|
|
1130
|
+
const baseName = controlType.split('.').pop()?.toLowerCase();
|
|
1131
|
+
if (baseName && lowerType.includes(baseName))
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Find all downstream nodes (nodes to the right of the dead-end)
|
|
1138
|
+
*/
|
|
1139
|
+
function findDownstreamNodes(workflow, sourceNode) {
|
|
1140
|
+
return workflow.nodes.filter((node) => {
|
|
1141
|
+
// Must be to the right of source
|
|
1142
|
+
if (node.position[0] <= sourceNode.position[0])
|
|
1143
|
+
return false;
|
|
1144
|
+
// Must not be the same node
|
|
1145
|
+
if (node.id === sourceNode.id || node.name === sourceNode.name)
|
|
1146
|
+
return false;
|
|
1147
|
+
// Exclude sticky notes
|
|
1148
|
+
const lowerType = node.type.toLowerCase();
|
|
1149
|
+
if (lowerType.includes('stickynote'))
|
|
1150
|
+
return false;
|
|
1151
|
+
return true;
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Find the closest downstream node by Y-distance
|
|
1156
|
+
*/
|
|
1157
|
+
function findClosestDownstreamByY(downstreamNodes, sourceNode) {
|
|
1158
|
+
if (downstreamNodes.length === 0)
|
|
1159
|
+
return null;
|
|
1160
|
+
let closest = downstreamNodes[0];
|
|
1161
|
+
let minYDistance = calculateYDistance(closest, sourceNode);
|
|
1162
|
+
for (const node of downstreamNodes) {
|
|
1163
|
+
const yDistance = calculateYDistance(node, sourceNode);
|
|
1164
|
+
if (yDistance < minYDistance) {
|
|
1165
|
+
minYDistance = yDistance;
|
|
1166
|
+
closest = node;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return { node: closest, yDistance: minYDistance };
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* L6DeadEndFixerV2 - Position-based dead-end fixer with terminal detection
|
|
1173
|
+
*
|
|
1174
|
+
* RAG-2.2.108.2: Replaces the low-confidence handler with intelligent fixes:
|
|
1175
|
+
* 1. Skip valid terminal types (confidence 1.0, not an error)
|
|
1176
|
+
* 2. Single downstream → auto-connect (confidence 0.90)
|
|
1177
|
+
* 3. Multiple downstream → connect to closest-Y (confidence 0.75)
|
|
1178
|
+
* 4. Data transform dead-end → add NoOp (confidence 0.85)
|
|
1179
|
+
* 5. Flow control missing branch → flag (confidence 0.50)
|
|
1180
|
+
* 6. No downstream → flag for manual review (confidence 0.50)
|
|
1181
|
+
*
|
|
1182
|
+
* Expected Impact: ~40% of dead-end nodes auto-fixed
|
|
1183
|
+
*/
|
|
1184
|
+
export class L6DeadEndFixerV2 {
|
|
1185
|
+
name = 'L6DeadEndFixerV2';
|
|
1186
|
+
description = 'Position-based dead-end fixer with terminal detection';
|
|
1187
|
+
supportedCodes = ['L6_DEAD_END_NODE'];
|
|
1188
|
+
minConfidence = 0.50;
|
|
1189
|
+
canFix(problem) {
|
|
1190
|
+
return problem.level === 'L6' && problem.code === 'L6_DEAD_END_NODE';
|
|
1191
|
+
}
|
|
1192
|
+
fix(workflow, problem) {
|
|
1193
|
+
if (!this.canFix(problem)) {
|
|
1194
|
+
return createFailureResult(this.name, problem.code, 'Problem does not meet requirements for L6DeadEndFixerV2');
|
|
1195
|
+
}
|
|
1196
|
+
// Find the dead-end node
|
|
1197
|
+
const nodeNameFromDesc = extractNodeNameFromDescription(problem.description);
|
|
1198
|
+
const node = findNode(workflow, problem.node_id, problem.node_name) ||
|
|
1199
|
+
(nodeNameFromDesc ? findNode(workflow, undefined, nodeNameFromDesc) : undefined);
|
|
1200
|
+
if (!node) {
|
|
1201
|
+
return createFailureResult(this.name, problem.code, `Node not found: ${problem.node_id || problem.node_name || nodeNameFromDesc || 'unknown'}`);
|
|
1202
|
+
}
|
|
1203
|
+
// Step 1: Check if node type is valid terminal → SKIP (not an error)
|
|
1204
|
+
if (isValidTerminalType(node.type)) {
|
|
1205
|
+
return this.createValidTerminalResult(node, problem);
|
|
1206
|
+
}
|
|
1207
|
+
// Step 2-6: Find downstream nodes and apply appropriate fix
|
|
1208
|
+
const downstreamNodes = findDownstreamNodes(workflow, node);
|
|
1209
|
+
// No downstream nodes
|
|
1210
|
+
if (downstreamNodes.length === 0) {
|
|
1211
|
+
return this.handleNoDownstream(workflow, node, problem);
|
|
1212
|
+
}
|
|
1213
|
+
// Single downstream → auto-connect (confidence 0.90)
|
|
1214
|
+
if (downstreamNodes.length === 1) {
|
|
1215
|
+
return this.createSingleDownstreamConnection(workflow, node, downstreamNodes[0], problem);
|
|
1216
|
+
}
|
|
1217
|
+
// Multiple downstream → connect to closest-Y (confidence 0.75)
|
|
1218
|
+
return this.createClosestDownstreamConnection(workflow, node, downstreamNodes, problem);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Create result for valid terminal node skip
|
|
1222
|
+
*/
|
|
1223
|
+
createValidTerminalResult(node, problem) {
|
|
1224
|
+
logger.info('L6DeadEndFixerV2: Skipping valid terminal node', {
|
|
1225
|
+
node_name: node.name,
|
|
1226
|
+
node_type: node.type,
|
|
1227
|
+
});
|
|
1228
|
+
return {
|
|
1229
|
+
success: true,
|
|
1230
|
+
fixedWorkflow: undefined, // No workflow changes needed
|
|
1231
|
+
changes: [{
|
|
1232
|
+
change_type: 'parameter_update',
|
|
1233
|
+
node_id: node.id,
|
|
1234
|
+
node_name: node.name,
|
|
1235
|
+
field: 'status',
|
|
1236
|
+
old_value: 'dead_end',
|
|
1237
|
+
new_value: 'valid_terminal',
|
|
1238
|
+
confidence: DEAD_END_FIXER_CONFIG.VALID_TERMINAL_CONFIDENCE,
|
|
1239
|
+
description: `Skipped valid terminal node "${node.name}" (${node.type}) - not an error`,
|
|
1240
|
+
}],
|
|
1241
|
+
confidence: DEAD_END_FIXER_CONFIG.VALID_TERMINAL_CONFIDENCE,
|
|
1242
|
+
fixer_name: this.name,
|
|
1243
|
+
problem_code: problem.code,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Handle dead-end with no downstream nodes
|
|
1248
|
+
*/
|
|
1249
|
+
handleNoDownstream(workflow, node, problem) {
|
|
1250
|
+
// Data transform → add NoOp (confidence 0.85)
|
|
1251
|
+
if (isDataTransformType(node.type)) {
|
|
1252
|
+
return this.addNoOpEndpoint(workflow, node, problem);
|
|
1253
|
+
}
|
|
1254
|
+
// Flow control → flag missing branch (confidence 0.50)
|
|
1255
|
+
if (isFlowControlType(node.type)) {
|
|
1256
|
+
return this.createFlowControlFlag(node, problem);
|
|
1257
|
+
}
|
|
1258
|
+
// Otherwise → flag for manual review (confidence 0.50)
|
|
1259
|
+
logger.info('L6DeadEndFixerV2: Flagging dead-end for manual review', {
|
|
1260
|
+
node_name: node.name,
|
|
1261
|
+
node_type: node.type,
|
|
1262
|
+
});
|
|
1263
|
+
return {
|
|
1264
|
+
success: false,
|
|
1265
|
+
changes: [],
|
|
1266
|
+
reason: `Dead-end node "${node.name}" (${node.type}) has no downstream nodes - requires manual review`,
|
|
1267
|
+
confidence: DEAD_END_FIXER_CONFIG.MANUAL_REVIEW_CONFIDENCE,
|
|
1268
|
+
fixer_name: this.name,
|
|
1269
|
+
problem_code: problem.code,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Add NoOp endpoint for data transform dead-ends
|
|
1274
|
+
*/
|
|
1275
|
+
addNoOpEndpoint(workflow, sourceNode, problem) {
|
|
1276
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
1277
|
+
// Create NoOp node
|
|
1278
|
+
const noOpNode = {
|
|
1279
|
+
id: generateNodeId(),
|
|
1280
|
+
type: 'n8n-nodes-base.noOp',
|
|
1281
|
+
name: `${sourceNode.name} End`,
|
|
1282
|
+
position: [sourceNode.position[0] + 200, sourceNode.position[1]],
|
|
1283
|
+
parameters: {},
|
|
1284
|
+
typeVersion: 1,
|
|
1285
|
+
};
|
|
1286
|
+
// Add node to workflow
|
|
1287
|
+
fixedWorkflow.nodes.push(noOpNode);
|
|
1288
|
+
// Connect source to NoOp
|
|
1289
|
+
if (!fixedWorkflow.connections[sourceNode.name]) {
|
|
1290
|
+
fixedWorkflow.connections[sourceNode.name] = { main: [] };
|
|
1291
|
+
}
|
|
1292
|
+
if (!fixedWorkflow.connections[sourceNode.name].main) {
|
|
1293
|
+
fixedWorkflow.connections[sourceNode.name].main = [];
|
|
1294
|
+
}
|
|
1295
|
+
const outputs = fixedWorkflow.connections[sourceNode.name].main;
|
|
1296
|
+
if (outputs.length === 0) {
|
|
1297
|
+
outputs.push([]);
|
|
1298
|
+
}
|
|
1299
|
+
outputs[0].push({
|
|
1300
|
+
node: noOpNode.name,
|
|
1301
|
+
type: 'main',
|
|
1302
|
+
index: 0,
|
|
1303
|
+
});
|
|
1304
|
+
const change = {
|
|
1305
|
+
change_type: 'parameter_update',
|
|
1306
|
+
node_id: sourceNode.id,
|
|
1307
|
+
node_name: sourceNode.name,
|
|
1308
|
+
field: 'connections',
|
|
1309
|
+
old_value: null,
|
|
1310
|
+
new_value: { addedNode: noOpNode.name, type: 'noOp' },
|
|
1311
|
+
confidence: DEAD_END_FIXER_CONFIG.DATA_TRANSFORM_NOOP_CONFIDENCE,
|
|
1312
|
+
description: `Added NoOp endpoint for data transform dead-end "${sourceNode.name}"`,
|
|
1313
|
+
};
|
|
1314
|
+
logger.info('L6DeadEndFixerV2: Added NoOp endpoint for data transform', {
|
|
1315
|
+
source: sourceNode.name,
|
|
1316
|
+
noOpNode: noOpNode.name,
|
|
1317
|
+
});
|
|
1318
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], DEAD_END_FIXER_CONFIG.DATA_TRANSFORM_NOOP_CONFIDENCE);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Create flow control missing branch flag
|
|
1322
|
+
*/
|
|
1323
|
+
createFlowControlFlag(node, problem) {
|
|
1324
|
+
logger.info('L6DeadEndFixerV2: Flagging flow control missing branch', {
|
|
1325
|
+
node_name: node.name,
|
|
1326
|
+
node_type: node.type,
|
|
1327
|
+
});
|
|
1328
|
+
return {
|
|
1329
|
+
success: true,
|
|
1330
|
+
fixedWorkflow: undefined, // No auto-fix, just flagged
|
|
1331
|
+
changes: [{
|
|
1332
|
+
change_type: 'parameter_update',
|
|
1333
|
+
node_id: node.id,
|
|
1334
|
+
node_name: node.name,
|
|
1335
|
+
field: 'status',
|
|
1336
|
+
old_value: 'dead_end',
|
|
1337
|
+
new_value: 'missing_branch',
|
|
1338
|
+
confidence: DEAD_END_FIXER_CONFIG.FLOW_CONTROL_FLAG_CONFIDENCE,
|
|
1339
|
+
description: `Flow control node "${node.name}" has missing output branch - review required`,
|
|
1340
|
+
}],
|
|
1341
|
+
confidence: DEAD_END_FIXER_CONFIG.FLOW_CONTROL_FLAG_CONFIDENCE,
|
|
1342
|
+
fixer_name: this.name,
|
|
1343
|
+
problem_code: problem.code,
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Create single downstream connection (confidence 0.90)
|
|
1348
|
+
*/
|
|
1349
|
+
createSingleDownstreamConnection(workflow, sourceNode, targetNode, problem) {
|
|
1350
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
1351
|
+
// Ensure connections object exists
|
|
1352
|
+
if (!fixedWorkflow.connections[sourceNode.name]) {
|
|
1353
|
+
fixedWorkflow.connections[sourceNode.name] = { main: [] };
|
|
1354
|
+
}
|
|
1355
|
+
if (!fixedWorkflow.connections[sourceNode.name].main) {
|
|
1356
|
+
fixedWorkflow.connections[sourceNode.name].main = [];
|
|
1357
|
+
}
|
|
1358
|
+
// Add connection to first output
|
|
1359
|
+
const outputs = fixedWorkflow.connections[sourceNode.name].main;
|
|
1360
|
+
if (outputs.length === 0) {
|
|
1361
|
+
outputs.push([]);
|
|
1362
|
+
}
|
|
1363
|
+
outputs[0].push({
|
|
1364
|
+
node: targetNode.name,
|
|
1365
|
+
type: 'main',
|
|
1366
|
+
index: 0,
|
|
1367
|
+
});
|
|
1368
|
+
const yDistance = calculateYDistance(sourceNode, targetNode);
|
|
1369
|
+
const change = {
|
|
1370
|
+
change_type: 'parameter_update',
|
|
1371
|
+
node_id: sourceNode.id,
|
|
1372
|
+
node_name: sourceNode.name,
|
|
1373
|
+
field: 'connections',
|
|
1374
|
+
old_value: null,
|
|
1375
|
+
new_value: {
|
|
1376
|
+
source: sourceNode.name,
|
|
1377
|
+
target: targetNode.name,
|
|
1378
|
+
type: 'main',
|
|
1379
|
+
index: 0,
|
|
1380
|
+
},
|
|
1381
|
+
confidence: DEAD_END_FIXER_CONFIG.SINGLE_DOWNSTREAM_CONFIDENCE,
|
|
1382
|
+
description: `Connected dead-end "${sourceNode.name}" to single downstream "${targetNode.name}" (Y-distance: ${yDistance}px)`,
|
|
1383
|
+
};
|
|
1384
|
+
logger.info('L6DeadEndFixerV2: Applied single downstream connection', {
|
|
1385
|
+
source: sourceNode.name,
|
|
1386
|
+
target: targetNode.name,
|
|
1387
|
+
y_distance: yDistance,
|
|
1388
|
+
});
|
|
1389
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], DEAD_END_FIXER_CONFIG.SINGLE_DOWNSTREAM_CONFIDENCE);
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Create closest-Y downstream connection (confidence 0.75)
|
|
1393
|
+
*/
|
|
1394
|
+
createClosestDownstreamConnection(workflow, sourceNode, downstreamNodes, problem) {
|
|
1395
|
+
const closest = findClosestDownstreamByY(downstreamNodes, sourceNode);
|
|
1396
|
+
if (!closest) {
|
|
1397
|
+
return createFailureResult(this.name, problem.code, `Could not determine closest downstream for "${sourceNode.name}"`);
|
|
1398
|
+
}
|
|
1399
|
+
const fixedWorkflow = cloneWorkflow(workflow);
|
|
1400
|
+
// Ensure connections object exists
|
|
1401
|
+
if (!fixedWorkflow.connections[sourceNode.name]) {
|
|
1402
|
+
fixedWorkflow.connections[sourceNode.name] = { main: [] };
|
|
1403
|
+
}
|
|
1404
|
+
if (!fixedWorkflow.connections[sourceNode.name].main) {
|
|
1405
|
+
fixedWorkflow.connections[sourceNode.name].main = [];
|
|
1406
|
+
}
|
|
1407
|
+
// Add connection to first output
|
|
1408
|
+
const outputs = fixedWorkflow.connections[sourceNode.name].main;
|
|
1409
|
+
if (outputs.length === 0) {
|
|
1410
|
+
outputs.push([]);
|
|
1411
|
+
}
|
|
1412
|
+
outputs[0].push({
|
|
1413
|
+
node: closest.node.name,
|
|
1414
|
+
type: 'main',
|
|
1415
|
+
index: 0,
|
|
1416
|
+
});
|
|
1417
|
+
const change = {
|
|
1418
|
+
change_type: 'parameter_update',
|
|
1419
|
+
node_id: sourceNode.id,
|
|
1420
|
+
node_name: sourceNode.name,
|
|
1421
|
+
field: 'connections',
|
|
1422
|
+
old_value: null,
|
|
1423
|
+
new_value: {
|
|
1424
|
+
source: sourceNode.name,
|
|
1425
|
+
target: closest.node.name,
|
|
1426
|
+
type: 'main',
|
|
1427
|
+
index: 0,
|
|
1428
|
+
},
|
|
1429
|
+
confidence: DEAD_END_FIXER_CONFIG.MULTIPLE_DOWNSTREAM_CONFIDENCE,
|
|
1430
|
+
description: `Connected dead-end "${sourceNode.name}" to closest downstream "${closest.node.name}" (Y-distance: ${closest.yDistance}px, ${downstreamNodes.length} candidates)`,
|
|
1431
|
+
};
|
|
1432
|
+
logger.info('L6DeadEndFixerV2: Applied closest-Y downstream connection', {
|
|
1433
|
+
source: sourceNode.name,
|
|
1434
|
+
target: closest.node.name,
|
|
1435
|
+
y_distance: closest.yDistance,
|
|
1436
|
+
candidates: downstreamNodes.length,
|
|
1437
|
+
});
|
|
1438
|
+
return createSuccessResult(this.name, problem.code, fixedWorkflow, [change], DEAD_END_FIXER_CONFIG.MULTIPLE_DOWNSTREAM_CONFIDENCE);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
// Export V2 instance
|
|
1442
|
+
export const l6DeadEndFixerV2 = new L6DeadEndFixerV2();
|
|
1443
|
+
// Export configuration for external use
|
|
1444
|
+
export { DEAD_END_FIXER_CONFIG, VALID_TERMINAL_TYPES, DATA_TRANSFORM_TYPES, FLOW_CONTROL_TYPES };
|
|
1445
|
+
// ============================================================
|
|
1446
|
+
// EXPORTS
|
|
1447
|
+
// ============================================================
|
|
1448
|
+
// Export singleton instances
|
|
1449
|
+
export const l6RetryFixer = new L6RetryFixer();
|
|
1450
|
+
export const l6ErrorOutputFixer = new L6ErrorOutputFixer();
|
|
1451
|
+
/** @deprecated Use l6DeadEndFixerV2 instead - legacy fixer with lower confidence */
|
|
1452
|
+
export const l6DeadEndFixer = new L6DeadEndFixer();
|
|
1453
|
+
export const l6ExpressionFixer = new L6ExpressionFixer();
|
|
1454
|
+
export const l6TriggerFixer = new L6TriggerFixer();
|
|
1455
|
+
export const l6HardcodedUrlHandler = new L6HardcodedUrlHandler();
|
|
1456
|
+
export const l6UnreachableNodeFixerV2 = new L6UnreachableNodeFixerV2();
|
|
1457
|
+
/** @deprecated Use l6UnreachableNodeFixerV2 instead */
|
|
1458
|
+
export const l6UnreachableNodeHandler = new L6UnreachableNodeHandler();
|
|
1459
|
+
// Export configuration for external use
|
|
1460
|
+
export { UNREACHABLE_FIXER_CONFIG };
|
|
1461
|
+
/**
|
|
1462
|
+
* All L6 pattern fixers for convenient import
|
|
1463
|
+
* RAG-2.2.108.1: Updated to use V2 unreachable node fixer
|
|
1464
|
+
* RAG-2.2.108.2: Updated to use V2 dead-end fixer with terminal detection
|
|
1465
|
+
*/
|
|
1466
|
+
export const l6PatternFixers = [
|
|
1467
|
+
l6RetryFixer,
|
|
1468
|
+
l6ErrorOutputFixer,
|
|
1469
|
+
l6DeadEndFixerV2, // V2 replaces legacy fixer
|
|
1470
|
+
l6ExpressionFixer,
|
|
1471
|
+
l6TriggerFixer,
|
|
1472
|
+
l6HardcodedUrlHandler,
|
|
1473
|
+
l6UnreachableNodeFixerV2, // V2 replaces legacy handler
|
|
1474
|
+
];
|
|
1475
|
+
//# sourceMappingURL=l6-pattern-fixer.js.map
|