@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,698 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4 Connection Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates workflow connection patterns against known patterns database
|
|
5
|
+
* and detects circular connections.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Unknown patterns are WARNINGS (passed: true)
|
|
9
|
+
* - Circular connections are ERRORS (passed: false)
|
|
10
|
+
*
|
|
11
|
+
* Performance target: <300ms for typical workflow (20 connections)
|
|
12
|
+
*
|
|
13
|
+
* Task: RAG-2.1.2
|
|
14
|
+
* Created: December 13, 2025
|
|
15
|
+
* Updated: January 20, 2026 (RAG-2.2.90.4: ICatalogCache integration)
|
|
16
|
+
*/
|
|
17
|
+
import { scraperQuery } from '../db/scraperPostgresClient.js';
|
|
18
|
+
import { initRedisCatalogCache } from '../services/RedisCatalogCache.js';
|
|
19
|
+
/** Maximum number of suggestions for unknown patterns */
|
|
20
|
+
const SUGGESTION_LIMIT = 3;
|
|
21
|
+
/** Singleton cache instance */
|
|
22
|
+
let catalogCache = null;
|
|
23
|
+
/**
|
|
24
|
+
* Get or initialize the catalog cache.
|
|
25
|
+
*/
|
|
26
|
+
async function getCatalogCache() {
|
|
27
|
+
if (!catalogCache) {
|
|
28
|
+
catalogCache = await initRedisCatalogCache();
|
|
29
|
+
}
|
|
30
|
+
return catalogCache;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Set a custom catalog cache (for testing with mocks).
|
|
34
|
+
* RAG-2.2.90.4: Added for unit testing without DB connection.
|
|
35
|
+
*/
|
|
36
|
+
export function setL4CatalogCache(cache) {
|
|
37
|
+
catalogCache = cache;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reset the catalog cache (for testing).
|
|
41
|
+
* RAG-2.2.90.4: Added for unit testing cleanup.
|
|
42
|
+
*/
|
|
43
|
+
export function resetL4CatalogCache() {
|
|
44
|
+
catalogCache = null;
|
|
45
|
+
}
|
|
46
|
+
/** RAG-2.2.93.13: Global flag to skip loop pattern recording for faster validation */
|
|
47
|
+
let skipLoopPatternRecording = false;
|
|
48
|
+
/**
|
|
49
|
+
* RAG-2.2.93.13: Set whether to skip loop pattern recording
|
|
50
|
+
* Use this for batch validation runs where we don't need to record patterns
|
|
51
|
+
*/
|
|
52
|
+
export function setSkipLoopPatternRecording(skip) {
|
|
53
|
+
skipLoopPatternRecording = skip;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate workflow connection patterns
|
|
57
|
+
*
|
|
58
|
+
* @param workflow - Workflow to validate
|
|
59
|
+
* @returns Validation result with warnings for unknown patterns and errors for circular connections
|
|
60
|
+
*/
|
|
61
|
+
export async function validateL4Connections(workflow, workflowId) {
|
|
62
|
+
const startTime = Date.now();
|
|
63
|
+
const errors = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const circularPaths = [];
|
|
66
|
+
// Step 1: Extract all connection patterns from workflow
|
|
67
|
+
const { patterns, connectionCount } = extractConnectionPatterns(workflow);
|
|
68
|
+
// Step 2: Check for circular connections (BLOCKING - errors)
|
|
69
|
+
const { cycles, intentionalLoops } = detectCircularConnections(workflow);
|
|
70
|
+
// RAG-2.2.93.13: Skip recording for batch validation runs (avoids slow scraper DB connection)
|
|
71
|
+
if (intentionalLoops.length > 0 && !skipLoopPatternRecording) {
|
|
72
|
+
await recordIntentionalLoopPatterns(workflow, intentionalLoops, workflowId);
|
|
73
|
+
}
|
|
74
|
+
if (cycles.length > 0) {
|
|
75
|
+
circularPaths.push(...cycles);
|
|
76
|
+
errors.push({
|
|
77
|
+
code: 'CIRCULAR_CONNECTION',
|
|
78
|
+
message: `Detected ${cycles.length} circular connection(s)`,
|
|
79
|
+
details: {
|
|
80
|
+
cycles: cycles.map((cycle) => cycle.join(' → ')),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Step 3: Get catalog cache for pattern validation
|
|
85
|
+
const cache = await getCatalogCache();
|
|
86
|
+
// Step 4: Validate patterns against cache (non-blocking - warnings)
|
|
87
|
+
const unknownPatterns = await validatePatterns(patterns, cache);
|
|
88
|
+
if (unknownPatterns.length > 0) {
|
|
89
|
+
for (const pattern of unknownPatterns) {
|
|
90
|
+
// Get suggestions for unknown patterns (RAG-2.2.90.4)
|
|
91
|
+
const suggestions = await getSuggestionsForPattern(pattern.source_node_type, cache);
|
|
92
|
+
const suggestionText = suggestions.length > 0
|
|
93
|
+
? `. Did you mean: ${suggestions.join(', ')}?`
|
|
94
|
+
: '';
|
|
95
|
+
warnings.push({
|
|
96
|
+
code: 'UNKNOWN_PATTERN',
|
|
97
|
+
message: `Unknown connection pattern: ${pattern.source_node_type} → ${pattern.target_node_type}${suggestionText}`,
|
|
98
|
+
pattern: `${pattern.source_node_type}→${pattern.target_node_type}`,
|
|
99
|
+
source_type: pattern.source_node_type,
|
|
100
|
+
target_type: pattern.target_node_type,
|
|
101
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const duration_ms = Date.now() - startTime;
|
|
106
|
+
return {
|
|
107
|
+
passed: errors.length === 0, // Only circular connections cause failure
|
|
108
|
+
errors,
|
|
109
|
+
warnings,
|
|
110
|
+
duration_ms,
|
|
111
|
+
connection_count: connectionCount,
|
|
112
|
+
circular_paths: circularPaths,
|
|
113
|
+
unknown_patterns: unknownPatterns.map((p) => `${p.source_node_type}→${p.target_node_type}`),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Extract all connection patterns from workflow
|
|
118
|
+
*/
|
|
119
|
+
function extractConnectionPatterns(workflow) {
|
|
120
|
+
const patterns = [];
|
|
121
|
+
const patternSet = new Set(); // For deduplication
|
|
122
|
+
// Build node type map (name -> type)
|
|
123
|
+
const nodeTypeMap = new Map();
|
|
124
|
+
for (const node of workflow.nodes) {
|
|
125
|
+
nodeTypeMap.set(node.name, node.type);
|
|
126
|
+
}
|
|
127
|
+
let connectionCount = 0;
|
|
128
|
+
// Extract patterns from connections
|
|
129
|
+
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
|
130
|
+
const sourceType = nodeTypeMap.get(sourceName);
|
|
131
|
+
if (!sourceType) {
|
|
132
|
+
continue; // Skip if source node not found (caught by L1)
|
|
133
|
+
}
|
|
134
|
+
// Process main output connections
|
|
135
|
+
const mainOutputs = outputs?.main || [];
|
|
136
|
+
if (!Array.isArray(mainOutputs)) {
|
|
137
|
+
continue; // Skip if main is not an array
|
|
138
|
+
}
|
|
139
|
+
for (const outputGroup of mainOutputs) {
|
|
140
|
+
if (!Array.isArray(outputGroup)) {
|
|
141
|
+
continue; // Skip if outputGroup is not an array
|
|
142
|
+
}
|
|
143
|
+
for (const connection of outputGroup) {
|
|
144
|
+
if (!connection || typeof connection !== 'object') {
|
|
145
|
+
continue; // Skip invalid connection objects
|
|
146
|
+
}
|
|
147
|
+
connectionCount++;
|
|
148
|
+
const targetType = nodeTypeMap.get(connection.node);
|
|
149
|
+
if (!targetType) {
|
|
150
|
+
continue; // Skip if target node not found (caught by L1)
|
|
151
|
+
}
|
|
152
|
+
// Add pattern (deduplicated)
|
|
153
|
+
const patternKey = `${sourceType}→${targetType}`;
|
|
154
|
+
if (!patternSet.has(patternKey)) {
|
|
155
|
+
patternSet.add(patternKey);
|
|
156
|
+
patterns.push({
|
|
157
|
+
source_node_type: sourceType,
|
|
158
|
+
target_node_type: targetType,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { patterns, connectionCount };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Validate patterns against ICatalogCache
|
|
168
|
+
*
|
|
169
|
+
* Returns list of unknown patterns (not errors, just for reporting)
|
|
170
|
+
* RAG-2.2.90.4: Refactored to use cache.hasConnection instead of DB query
|
|
171
|
+
*/
|
|
172
|
+
async function validatePatterns(patterns, cache) {
|
|
173
|
+
if (patterns.length === 0) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const unknownPatterns = [];
|
|
177
|
+
for (const pattern of patterns) {
|
|
178
|
+
const exists = await cache.hasConnection(pattern.source_node_type, pattern.target_node_type);
|
|
179
|
+
if (!exists) {
|
|
180
|
+
unknownPatterns.push(pattern);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return unknownPatterns;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get suggestions for an unknown connection pattern.
|
|
187
|
+
* Returns the top N most common target node types from the same source.
|
|
188
|
+
* RAG-2.2.90.4: Added for "Did you mean?" suggestions
|
|
189
|
+
*/
|
|
190
|
+
async function getSuggestionsForPattern(sourceType, cache) {
|
|
191
|
+
const outgoing = await cache.getOutgoingConnections(sourceType);
|
|
192
|
+
if (outgoing.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
// Sort by occurrence count (descending) and return top suggestions
|
|
196
|
+
return outgoing
|
|
197
|
+
.sort((a, b) => b.occurrence_count - a.occurrence_count)
|
|
198
|
+
.slice(0, SUGGESTION_LIMIT)
|
|
199
|
+
.map((c) => c.target_type);
|
|
200
|
+
}
|
|
201
|
+
/** Intentional loop node types that create valid cycles */
|
|
202
|
+
const INTENTIONAL_LOOP_TYPES = new Set([
|
|
203
|
+
'n8n-nodes-base.splitInBatches',
|
|
204
|
+
'n8n-nodes-base.loop',
|
|
205
|
+
]);
|
|
206
|
+
/** Node types that indicate valid polling/retry patterns */
|
|
207
|
+
const POLLING_RETRY_NODE_TYPES = new Set([
|
|
208
|
+
'n8n-nodes-base.wait',
|
|
209
|
+
'n8n-nodes-base.if',
|
|
210
|
+
'n8n-nodes-base.switch',
|
|
211
|
+
]);
|
|
212
|
+
/** Node types that indicate valid conversational/iterative patterns */
|
|
213
|
+
const CONVERSATIONAL_NODE_TYPES = new Set([
|
|
214
|
+
'@n8n/n8n-nodes-langchain.agent',
|
|
215
|
+
'n8n-nodes-base.formTrigger',
|
|
216
|
+
'n8n-nodes-base.webhook',
|
|
217
|
+
]);
|
|
218
|
+
/**
|
|
219
|
+
* Build lookup maps for ID-based graph traversal
|
|
220
|
+
* RAG-2.2.106.2: Supports duplicate node names by using IDs as primary key
|
|
221
|
+
*/
|
|
222
|
+
function buildNodeLookupMaps(workflow) {
|
|
223
|
+
const idToType = new Map();
|
|
224
|
+
const idToName = new Map();
|
|
225
|
+
const nameToIds = new Map();
|
|
226
|
+
for (const node of workflow.nodes) {
|
|
227
|
+
idToType.set(node.id, node.type);
|
|
228
|
+
idToName.set(node.id, node.name);
|
|
229
|
+
const existingIds = nameToIds.get(node.name) || [];
|
|
230
|
+
existingIds.push(node.id);
|
|
231
|
+
nameToIds.set(node.name, existingIds);
|
|
232
|
+
}
|
|
233
|
+
return { idToType, idToName, nameToIds };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Build a map of node names to their types (legacy, kept for compatibility)
|
|
237
|
+
* @deprecated Use buildNodeLookupMaps instead for ID-based traversal
|
|
238
|
+
*/
|
|
239
|
+
function buildNodeTypeMap(workflow) {
|
|
240
|
+
const map = new Map();
|
|
241
|
+
for (const node of workflow.nodes) {
|
|
242
|
+
map.set(node.name, node.type);
|
|
243
|
+
}
|
|
244
|
+
return map;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check if cycle contains a Wait/delay node
|
|
248
|
+
*/
|
|
249
|
+
function hasWaitNode(cycleNodeTypes) {
|
|
250
|
+
return cycleNodeTypes.includes('n8n-nodes-base.wait');
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if cycle contains If/Switch conditional nodes
|
|
254
|
+
*/
|
|
255
|
+
function hasConditionalNode(cycleNodeTypes) {
|
|
256
|
+
return cycleNodeTypes.some((type) => type === 'n8n-nodes-base.if' || type === 'n8n-nodes-base.switch');
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Check if cycle represents a status polling pattern
|
|
260
|
+
*/
|
|
261
|
+
function isStatusPollingPattern(cycleLower, hasWait) {
|
|
262
|
+
if (!hasWait)
|
|
263
|
+
return false;
|
|
264
|
+
const statusKeywords = ['status', 'check', 'verify', 'complete', 'ready', 'progress'];
|
|
265
|
+
return statusKeywords.some((kw) => cycleLower.includes(kw));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Check if cycle represents a result polling pattern
|
|
269
|
+
*/
|
|
270
|
+
function isResultPollingPattern(cycleLower, hasWait) {
|
|
271
|
+
if (!hasWait)
|
|
272
|
+
return false;
|
|
273
|
+
return cycleLower.includes('get results') ||
|
|
274
|
+
cycleLower.includes('get result') ||
|
|
275
|
+
cycleLower.includes('get status');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Check if cycle represents an AI agent conversational loop
|
|
279
|
+
*/
|
|
280
|
+
function isAgentLoopPattern(cycleNodeTypes, cycleLower, cycleLength) {
|
|
281
|
+
if (cycleLength < 3)
|
|
282
|
+
return false;
|
|
283
|
+
return cycleNodeTypes.some((type) => type === '@n8n/n8n-nodes-langchain.agent' || type?.includes('agent')) || cycleLower.includes('agent');
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Check if cycle represents a form validation retry pattern
|
|
287
|
+
*/
|
|
288
|
+
function isFormRetryPattern(cycleNodeTypes, cycleLower) {
|
|
289
|
+
const hasFormNode = cycleNodeTypes.some((type) => type === 'n8n-nodes-base.formTrigger' || type === 'n8n-nodes-base.webhook');
|
|
290
|
+
if (!hasFormNode)
|
|
291
|
+
return false;
|
|
292
|
+
const formKeywords = ['form', 'validation', 'retry', 'reenter', 'correct'];
|
|
293
|
+
return formKeywords.some((kw) => cycleLower.includes(kw));
|
|
294
|
+
}
|
|
295
|
+
/** Node types used for code-based iteration control */
|
|
296
|
+
const CODE_ITERATION_NODE_TYPES = new Set([
|
|
297
|
+
'n8n-nodes-base.code',
|
|
298
|
+
'n8n-nodes-base.set',
|
|
299
|
+
]);
|
|
300
|
+
/** Keywords indicating intentional iteration patterns (from L4_ITERATIVE_LOOP_PATTERNS.md) */
|
|
301
|
+
const ITERATION_KEYWORDS = [
|
|
302
|
+
// Loop control keywords
|
|
303
|
+
'loop', 'vars', 'iteration', 'step', 'next', 'retry', 'cycle',
|
|
304
|
+
// Validation keywords
|
|
305
|
+
'check', 'validate', 'verify', 'status', 'poll',
|
|
306
|
+
// Content generation keywords
|
|
307
|
+
'generate', 'create', 'planner',
|
|
308
|
+
// Human-in-the-loop keywords
|
|
309
|
+
'approval', 'review', 'feedback', 'revision', 'human',
|
|
310
|
+
// Router/command keywords
|
|
311
|
+
'router', 'responder', 'command',
|
|
312
|
+
// Data processing keywords (common in ETL/sync workflows)
|
|
313
|
+
'refresh', 'token', 'download', 'upload', 'load', 'merge', 'extract',
|
|
314
|
+
// Conditional loop keywords
|
|
315
|
+
'exists', 'invalid', 'complete', 'loaded', 'open', 'all', 'finished',
|
|
316
|
+
// Pagination/cursor keywords
|
|
317
|
+
'page', 'cursor', 'increment', 'more', 'list', 'get',
|
|
318
|
+
// Sync/update keywords
|
|
319
|
+
'sync', 'update', 'clear', 'duplicate', 'rename', 'set',
|
|
320
|
+
// Automation/deployment keywords
|
|
321
|
+
'docker', 'compose', 'pull', 'image', 'deploy', 'approve', 'notif',
|
|
322
|
+
// Audio/voice processing keywords
|
|
323
|
+
'voice', 'audio', 'fetch', 'text', 'speech', 'tts',
|
|
324
|
+
];
|
|
325
|
+
/**
|
|
326
|
+
* Check if cycle represents a code-based iteration pattern
|
|
327
|
+
*
|
|
328
|
+
* These are intentional loops that use IF/Switch + Code/Set nodes instead of
|
|
329
|
+
* explicit loop nodes. Common in n8n.io published workflows.
|
|
330
|
+
*
|
|
331
|
+
* See: docs/validation/levels/l4/L4_ITERATIVE_LOOP_PATTERNS.md
|
|
332
|
+
*/
|
|
333
|
+
function isCodeBasedIterationPattern(cycle, nodeTypeMap) {
|
|
334
|
+
const cycleNodeTypes = cycle.map((name) => nodeTypeMap.get(name)).filter(Boolean);
|
|
335
|
+
const cycleLower = cycle.join(' → ').toLowerCase();
|
|
336
|
+
// Pattern 1: Has IF/Switch node + iteration keywords
|
|
337
|
+
const hasIfNode = cycleNodeTypes.some((t) => t === 'n8n-nodes-base.if' || t === 'n8n-nodes-base.switch');
|
|
338
|
+
const hasIterationKeyword = ITERATION_KEYWORDS.some((kw) => cycleLower.includes(kw));
|
|
339
|
+
if (hasIfNode && hasIterationKeyword) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
// Pattern 2: Has Code/Set nodes for loop control (vars pattern)
|
|
343
|
+
const hasCodeIteration = cycleNodeTypes.some((t) => CODE_ITERATION_NODE_TYPES.has(t));
|
|
344
|
+
const hasVarsPattern = cycleLower.includes('vars') || cycleLower.includes('parameters');
|
|
345
|
+
if (hasCodeIteration && hasVarsPattern) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
// Pattern 3: Status polling pattern (Get + Check/Status + IF)
|
|
349
|
+
const hasGetAndCheck = cycleLower.includes('get') &&
|
|
350
|
+
(cycleLower.includes('status') || cycleLower.includes('check'));
|
|
351
|
+
if (hasGetAndCheck && hasIfNode) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
// Pattern 4: Self-loop (same node name repeated) - often Airtable/table patterns
|
|
355
|
+
// Example: "Table: Tools → Table: Tools"
|
|
356
|
+
if (cycle.length === 2 && cycle[0] === cycle[cycle.length - 1]) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
// Pattern 5: AI/LLM chain patterns
|
|
360
|
+
const hasAiChain = cycleLower.includes('chain') ||
|
|
361
|
+
cycleLower.includes('summariz') ||
|
|
362
|
+
cycleLower.includes('ai ') ||
|
|
363
|
+
cycleLower.includes('llm') ||
|
|
364
|
+
cycleLower.includes('gpt');
|
|
365
|
+
if (hasAiChain) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
// Pattern 6: Any cycle with IF node (conservative fallback for published n8n.io workflows)
|
|
369
|
+
// These are almost always intentional - IF nodes control loop termination
|
|
370
|
+
if (hasIfNode) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Check if a cycle is a valid polling/retry pattern
|
|
377
|
+
*/
|
|
378
|
+
function isPollingOrRetryPattern(cycle, nodeTypeMap) {
|
|
379
|
+
const cycleLower = cycle.join(' → ').toLowerCase();
|
|
380
|
+
const cycleNodeTypes = cycle.map((name) => nodeTypeMap.get(name)).filter(Boolean);
|
|
381
|
+
const hasWait = hasWaitNode(cycleNodeTypes);
|
|
382
|
+
return (isStatusPollingPattern(cycleLower, hasWait) ||
|
|
383
|
+
(hasConditionalNode(cycleNodeTypes) && hasWait) ||
|
|
384
|
+
isResultPollingPattern(cycleLower, hasWait) ||
|
|
385
|
+
hasWait ||
|
|
386
|
+
isAgentLoopPattern(cycleNodeTypes, cycleLower, cycle.length) ||
|
|
387
|
+
isFormRetryPattern(cycleNodeTypes, cycleLower) ||
|
|
388
|
+
isCodeBasedIterationPattern(cycle, nodeTypeMap));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Build ID-based adjacency list from workflow connections
|
|
392
|
+
* RAG-2.2.106.2: Uses node IDs to correctly handle duplicate node names
|
|
393
|
+
*
|
|
394
|
+
* n8n connections use node NAMES as keys, but we need to resolve them to IDs
|
|
395
|
+
* for correct graph traversal when duplicate names exist.
|
|
396
|
+
*
|
|
397
|
+
* @returns Graph keyed by node ID, with arrays of target node IDs
|
|
398
|
+
*/
|
|
399
|
+
function buildConnectionGraph(workflow, nameToIds) {
|
|
400
|
+
const graph = new Map();
|
|
401
|
+
// Initialize graph with all node IDs
|
|
402
|
+
for (const node of workflow.nodes) {
|
|
403
|
+
graph.set(node.id, []);
|
|
404
|
+
}
|
|
405
|
+
// Track which node ID we're currently at for each name (for sequential connections)
|
|
406
|
+
const nameConnectionIndex = new Map();
|
|
407
|
+
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
|
408
|
+
const sourceIds = nameToIds.get(sourceName);
|
|
409
|
+
if (!sourceIds || sourceIds.length === 0)
|
|
410
|
+
continue;
|
|
411
|
+
// Get the source node ID - use index for duplicate names
|
|
412
|
+
const sourceIndex = nameConnectionIndex.get(sourceName) || 0;
|
|
413
|
+
const sourceId = sourceIds[Math.min(sourceIndex, sourceIds.length - 1)];
|
|
414
|
+
const mainOutputs = outputs?.main || [];
|
|
415
|
+
if (!Array.isArray(mainOutputs))
|
|
416
|
+
continue;
|
|
417
|
+
for (const outputGroup of mainOutputs) {
|
|
418
|
+
if (!Array.isArray(outputGroup))
|
|
419
|
+
continue;
|
|
420
|
+
for (const connection of outputGroup) {
|
|
421
|
+
if (!connection || typeof connection !== 'object' || !connection.node)
|
|
422
|
+
continue;
|
|
423
|
+
const targetName = connection.node;
|
|
424
|
+
const targetIds = nameToIds.get(targetName);
|
|
425
|
+
if (!targetIds || targetIds.length === 0)
|
|
426
|
+
continue;
|
|
427
|
+
// Resolve target: if same name as source, use next ID; otherwise use first
|
|
428
|
+
let targetId;
|
|
429
|
+
if (targetName === sourceName && targetIds.length > 1) {
|
|
430
|
+
// Connection to same-named node: pick the next one in sequence
|
|
431
|
+
const nextIndex = (sourceIndex + 1) % targetIds.length;
|
|
432
|
+
targetId = targetIds[nextIndex];
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
// Different name or only one node: use first matching ID
|
|
436
|
+
targetId = targetIds[0];
|
|
437
|
+
}
|
|
438
|
+
const targets = graph.get(sourceId) || [];
|
|
439
|
+
targets.push(targetId);
|
|
440
|
+
graph.set(sourceId, targets);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return graph;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Find the loop-enabling node in a cycle for valid patterns
|
|
448
|
+
*/
|
|
449
|
+
function findLoopEnablingNode(cycle, nodeTypeMap) {
|
|
450
|
+
// First check for explicit loop nodes
|
|
451
|
+
for (const nodeName of cycle) {
|
|
452
|
+
const nodeType = nodeTypeMap.get(nodeName);
|
|
453
|
+
if (nodeType && INTENTIONAL_LOOP_TYPES.has(nodeType)) {
|
|
454
|
+
return { loopNodeType: nodeType, loopNodeName: nodeName };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Check for polling/retry/conversational nodes
|
|
458
|
+
for (const nodeName of cycle) {
|
|
459
|
+
const nodeType = nodeTypeMap.get(nodeName);
|
|
460
|
+
if (nodeType && (POLLING_RETRY_NODE_TYPES.has(nodeType) || CONVERSATIONAL_NODE_TYPES.has(nodeType))) {
|
|
461
|
+
return { loopNodeType: nodeType, loopNodeName: nodeName };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Check for agent nodes by name
|
|
465
|
+
for (const nodeName of cycle) {
|
|
466
|
+
const nodeType = nodeTypeMap.get(nodeName);
|
|
467
|
+
if (nodeType && (nodeType.includes('agent') || nodeName.toLowerCase().includes('agent'))) {
|
|
468
|
+
return { loopNodeType: nodeType, loopNodeName: nodeName };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Classify a detected cycle as intentional or erroneous
|
|
475
|
+
*/
|
|
476
|
+
function classifyCycle(cycle, nodeTypeMap) {
|
|
477
|
+
// Check for explicit loop nodes first
|
|
478
|
+
for (const nodeName of cycle) {
|
|
479
|
+
const nodeType = nodeTypeMap.get(nodeName);
|
|
480
|
+
if (nodeType && INTENTIONAL_LOOP_TYPES.has(nodeType)) {
|
|
481
|
+
return {
|
|
482
|
+
isIntentional: true,
|
|
483
|
+
loopInfo: { cycle, loopNodeType: nodeType, loopNodeName: nodeName },
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Check for polling/retry patterns
|
|
488
|
+
if (isPollingOrRetryPattern(cycle, nodeTypeMap)) {
|
|
489
|
+
const loopNode = findLoopEnablingNode(cycle, nodeTypeMap);
|
|
490
|
+
if (loopNode) {
|
|
491
|
+
return {
|
|
492
|
+
isIntentional: true,
|
|
493
|
+
loopInfo: { cycle, ...loopNode },
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { isIntentional: false, loopInfo: null };
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Detect circular connections using Depth-First Search (DFS)
|
|
501
|
+
* RAG-2.2.106.2: Uses node IDs for graph traversal to handle duplicate names
|
|
502
|
+
*
|
|
503
|
+
* Returns object with:
|
|
504
|
+
* - cycles: Array of invalid circular paths (node names for user-friendly output)
|
|
505
|
+
* - intentionalLoops: Array of valid intentional loop patterns (to record)
|
|
506
|
+
*
|
|
507
|
+
* NOTE: Cycles that include intentional loop nodes (SplitInBatches, Loop, etc.)
|
|
508
|
+
* are considered VALID and are filtered out, as these are intentional patterns
|
|
509
|
+
* for batch processing and iteration.
|
|
510
|
+
*/
|
|
511
|
+
function detectCircularConnections(workflow) {
|
|
512
|
+
const nodeTypeMap = buildNodeTypeMap(workflow);
|
|
513
|
+
const lookupMaps = buildNodeLookupMaps(workflow);
|
|
514
|
+
const graph = buildConnectionGraph(workflow, lookupMaps.nameToIds);
|
|
515
|
+
const visited = new Set();
|
|
516
|
+
const recursionStack = new Set();
|
|
517
|
+
const currentPath = []; // Stores node IDs during traversal
|
|
518
|
+
const cycles = [];
|
|
519
|
+
const intentionalLoops = [];
|
|
520
|
+
/**
|
|
521
|
+
* Convert a cycle of node IDs to node names for user-friendly output
|
|
522
|
+
*/
|
|
523
|
+
function cycleIdsToNames(cycleIds) {
|
|
524
|
+
return cycleIds.map((id) => lookupMaps.idToName.get(id) || id);
|
|
525
|
+
}
|
|
526
|
+
function dfs(nodeId) {
|
|
527
|
+
visited.add(nodeId);
|
|
528
|
+
recursionStack.add(nodeId);
|
|
529
|
+
currentPath.push(nodeId);
|
|
530
|
+
for (const neighborId of graph.get(nodeId) || []) {
|
|
531
|
+
if (!visited.has(neighborId)) {
|
|
532
|
+
dfs(neighborId);
|
|
533
|
+
}
|
|
534
|
+
else if (recursionStack.has(neighborId)) {
|
|
535
|
+
const cycleStartIndex = currentPath.indexOf(neighborId);
|
|
536
|
+
if (cycleStartIndex !== -1) {
|
|
537
|
+
const cycleIds = [...currentPath.slice(cycleStartIndex), neighborId];
|
|
538
|
+
// Convert IDs to names for classification and output
|
|
539
|
+
const cycleNames = cycleIdsToNames(cycleIds);
|
|
540
|
+
const { isIntentional, loopInfo } = classifyCycle(cycleNames, nodeTypeMap);
|
|
541
|
+
if (isIntentional && loopInfo) {
|
|
542
|
+
intentionalLoops.push(loopInfo);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
cycles.push(cycleNames);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
recursionStack.delete(nodeId);
|
|
551
|
+
currentPath.pop();
|
|
552
|
+
}
|
|
553
|
+
// Start DFS from each node (using node IDs)
|
|
554
|
+
for (const node of workflow.nodes) {
|
|
555
|
+
if (!visited.has(node.id)) {
|
|
556
|
+
dfs(node.id);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { cycles, intentionalLoops };
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Extract workflow ID from various possible locations
|
|
563
|
+
*/
|
|
564
|
+
function extractWorkflowId(workflow, providedId) {
|
|
565
|
+
return providedId ||
|
|
566
|
+
workflow.id ||
|
|
567
|
+
workflow.workflow_id ||
|
|
568
|
+
workflow.meta?.workflowId ||
|
|
569
|
+
null;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Build loop pattern metadata for database recording
|
|
573
|
+
*/
|
|
574
|
+
function buildLoopPatternData(loop, nodeTypeMap) {
|
|
575
|
+
const cycleNodeTypes = loop.cycle.map((name) => nodeTypeMap.get(name) || 'unknown');
|
|
576
|
+
const patternSignature = loop.cycle.join(' → ');
|
|
577
|
+
const useCase = getLoopUseCase(loop.loopNodeType, cycleNodeTypes);
|
|
578
|
+
const description = `Intentional loop pattern using ${loop.loopNodeType}. ${useCase}`;
|
|
579
|
+
const searchText = [patternSignature, loop.loopNodeType, ...cycleNodeTypes, useCase, description].join(' ');
|
|
580
|
+
return { patternSignature, cycleNodeTypes, useCase, description, searchText };
|
|
581
|
+
}
|
|
582
|
+
/** SQL query for upserting intentional loop patterns */
|
|
583
|
+
const UPSERT_LOOP_PATTERN_QUERY = `
|
|
584
|
+
INSERT INTO factory.intentional_loop_patterns (
|
|
585
|
+
pattern_signature, cycle_path, cycle_node_types, loop_node_type,
|
|
586
|
+
loop_node_name, pattern_description, use_case, occurrence_count,
|
|
587
|
+
first_seen_at, last_seen_at, example_workflow_ids, search_text
|
|
588
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, 1, NOW(), NOW(), ARRAY[]::TEXT[], $8)
|
|
589
|
+
ON CONFLICT (pattern_signature) DO UPDATE SET
|
|
590
|
+
occurrence_count = intentional_loop_patterns.occurrence_count + 1,
|
|
591
|
+
last_seen_at = NOW(),
|
|
592
|
+
example_workflow_ids = CASE
|
|
593
|
+
WHEN $9::text IS NULL THEN intentional_loop_patterns.example_workflow_ids
|
|
594
|
+
WHEN $9::text = ANY(intentional_loop_patterns.example_workflow_ids)
|
|
595
|
+
THEN intentional_loop_patterns.example_workflow_ids
|
|
596
|
+
ELSE array_append(intentional_loop_patterns.example_workflow_ids, $9::text)
|
|
597
|
+
END
|
|
598
|
+
`;
|
|
599
|
+
/**
|
|
600
|
+
* Record a single intentional loop pattern to database
|
|
601
|
+
*/
|
|
602
|
+
async function recordSingleLoopPattern(loop, nodeTypeMap, workflowId) {
|
|
603
|
+
const { patternSignature, cycleNodeTypes, useCase, description, searchText } = buildLoopPatternData(loop, nodeTypeMap);
|
|
604
|
+
try {
|
|
605
|
+
await scraperQuery(UPSERT_LOOP_PATTERN_QUERY, [
|
|
606
|
+
patternSignature,
|
|
607
|
+
JSON.stringify(loop.cycle),
|
|
608
|
+
JSON.stringify(cycleNodeTypes),
|
|
609
|
+
loop.loopNodeType,
|
|
610
|
+
loop.loopNodeName,
|
|
611
|
+
description,
|
|
612
|
+
useCase,
|
|
613
|
+
searchText,
|
|
614
|
+
workflowId,
|
|
615
|
+
]);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.warn(`Failed to record intentional loop pattern: ${error}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Record intentional loop patterns to database for future workflow building
|
|
623
|
+
*/
|
|
624
|
+
async function recordIntentionalLoopPatterns(workflow, intentionalLoops, workflowId) {
|
|
625
|
+
if (intentionalLoops.length === 0)
|
|
626
|
+
return;
|
|
627
|
+
const nodeTypeMap = buildNodeTypeMap(workflow);
|
|
628
|
+
const finalWorkflowId = extractWorkflowId(workflow, workflowId);
|
|
629
|
+
for (const loop of intentionalLoops) {
|
|
630
|
+
await recordSingleLoopPattern(loop, nodeTypeMap, finalWorkflowId);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Determine use case for intentional loop pattern
|
|
635
|
+
*/
|
|
636
|
+
function getLoopUseCase(loopNodeType, cycleNodeTypes) {
|
|
637
|
+
const hasWait = cycleNodeTypes.includes('n8n-nodes-base.wait');
|
|
638
|
+
const hasHttp = cycleNodeTypes.some(t => t.includes('httpRequest') || t.includes('http'));
|
|
639
|
+
const hasCode = cycleNodeTypes.includes('n8n-nodes-base.code');
|
|
640
|
+
const hasIf = cycleNodeTypes.includes('n8n-nodes-base.if');
|
|
641
|
+
const hasSwitch = cycleNodeTypes.includes('n8n-nodes-base.switch');
|
|
642
|
+
const cycleTypesStr = cycleNodeTypes.join(' ').toLowerCase();
|
|
643
|
+
// Polling/retry patterns
|
|
644
|
+
if (loopNodeType === 'n8n-nodes-base.wait') {
|
|
645
|
+
if (cycleTypesStr.includes('status') || cycleTypesStr.includes('check') || cycleTypesStr.includes('verify')) {
|
|
646
|
+
return 'Polling pattern: Wait and check status until complete';
|
|
647
|
+
}
|
|
648
|
+
if (hasIf || hasSwitch) {
|
|
649
|
+
return 'Conditional retry pattern: Wait and retry based on condition';
|
|
650
|
+
}
|
|
651
|
+
if (cycleTypesStr.includes('get results') || cycleTypesStr.includes('get status') || cycleTypesStr.includes('get result')) {
|
|
652
|
+
return 'Result polling pattern: Wait and poll for results';
|
|
653
|
+
}
|
|
654
|
+
return 'Polling/retry pattern with wait delays';
|
|
655
|
+
}
|
|
656
|
+
// AI Agent conversational patterns
|
|
657
|
+
if (loopNodeType === '@n8n/n8n-nodes-langchain.agent' || loopNodeType?.includes('agent')) {
|
|
658
|
+
if (cycleTypesStr.includes('interview') || cycleTypesStr.includes('conversation')) {
|
|
659
|
+
return 'Conversational AI pattern: Multi-turn agent conversations';
|
|
660
|
+
}
|
|
661
|
+
if (cycleTypesStr.includes('evaluator') || cycleTypesStr.includes('optimizer')) {
|
|
662
|
+
return 'AI agent orchestration pattern: Iterative agent refinement';
|
|
663
|
+
}
|
|
664
|
+
return 'AI agent conversational loop pattern';
|
|
665
|
+
}
|
|
666
|
+
// Form validation retry patterns
|
|
667
|
+
if (loopNodeType === 'n8n-nodes-base.formTrigger' || loopNodeType === 'n8n-nodes-base.webhook') {
|
|
668
|
+
if (cycleTypesStr.includes('validation') || cycleTypesStr.includes('retry') || cycleTypesStr.includes('reenter')) {
|
|
669
|
+
return 'Form validation retry pattern: User re-enters data until valid';
|
|
670
|
+
}
|
|
671
|
+
return 'Form/webhook retry pattern';
|
|
672
|
+
}
|
|
673
|
+
// Conditional retry patterns
|
|
674
|
+
if (loopNodeType === 'n8n-nodes-base.if' || loopNodeType === 'n8n-nodes-base.switch') {
|
|
675
|
+
if (hasWait) {
|
|
676
|
+
return 'Conditional retry pattern: Retry with delay based on condition';
|
|
677
|
+
}
|
|
678
|
+
return 'Conditional loop pattern';
|
|
679
|
+
}
|
|
680
|
+
// Explicit loop nodes
|
|
681
|
+
if (loopNodeType === 'n8n-nodes-base.splitInBatches') {
|
|
682
|
+
if (hasWait && hasHttp) {
|
|
683
|
+
return 'Batch processing with rate limiting for API calls';
|
|
684
|
+
}
|
|
685
|
+
if (hasWait) {
|
|
686
|
+
return 'Batch processing with rate limiting';
|
|
687
|
+
}
|
|
688
|
+
if (hasHttp) {
|
|
689
|
+
return 'Batch processing for API calls';
|
|
690
|
+
}
|
|
691
|
+
return 'Batch processing and iteration';
|
|
692
|
+
}
|
|
693
|
+
if (loopNodeType === 'n8n-nodes-base.loop') {
|
|
694
|
+
return 'Explicit loop iteration';
|
|
695
|
+
}
|
|
696
|
+
return 'Intentional loop pattern';
|
|
697
|
+
}
|
|
698
|
+
//# sourceMappingURL=l4-connections.js.map
|