@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.
Files changed (319) hide show
  1. package/AGENT_INSTALL.md +223 -0
  2. package/CHANGELOG.md +38 -0
  3. package/LICENSE +21 -0
  4. package/README.md +187 -0
  5. package/dist/autofix/suggestion-fixers/deprecated-node-fixer.js +465 -0
  6. package/dist/autofix/suggestion-fixers/deprecated-node-fixer.js.map +1 -0
  7. package/dist/autofix/suggestion-fixers/fixer-registry.js +495 -0
  8. package/dist/autofix/suggestion-fixers/fixer-registry.js.map +1 -0
  9. package/dist/autofix/suggestion-fixers/l1-structure-fixer.js +639 -0
  10. package/dist/autofix/suggestion-fixers/l1-structure-fixer.js.map +1 -0
  11. package/dist/autofix/suggestion-fixers/l4-connection-fixer.js +449 -0
  12. package/dist/autofix/suggestion-fixers/l4-connection-fixer.js.map +1 -0
  13. package/dist/autofix/suggestion-fixers/l5-parameter-fixer.js +575 -0
  14. package/dist/autofix/suggestion-fixers/l5-parameter-fixer.js.map +1 -0
  15. package/dist/autofix/suggestion-fixers/l5-typeversion-fixer.js +431 -0
  16. package/dist/autofix/suggestion-fixers/l5-typeversion-fixer.js.map +1 -0
  17. package/dist/autofix/suggestion-fixers/l5-webhook-path-fixer.js +356 -0
  18. package/dist/autofix/suggestion-fixers/l5-webhook-path-fixer.js.map +1 -0
  19. package/dist/autofix/suggestion-fixers/l6-ai-tool-variant-fixer.js +618 -0
  20. package/dist/autofix/suggestion-fixers/l6-ai-tool-variant-fixer.js.map +1 -0
  21. package/dist/autofix/suggestion-fixers/l6-pattern-fixer.js +1475 -0
  22. package/dist/autofix/suggestion-fixers/l6-pattern-fixer.js.map +1 -0
  23. package/dist/autofix/suggestion-fixers/llm-fixer.js +716 -0
  24. package/dist/autofix/suggestion-fixers/llm-fixer.js.map +1 -0
  25. package/dist/autofix/suggestion-fixers/missing-credential-fixer.js +336 -0
  26. package/dist/autofix/suggestion-fixers/missing-credential-fixer.js.map +1 -0
  27. package/dist/autofix/suggestion-fixers/types.js +29 -0
  28. package/dist/autofix/suggestion-fixers/types.js.map +1 -0
  29. package/dist/autofix/suggestion-fixers/typo-fixer.js +197 -0
  30. package/dist/autofix/suggestion-fixers/typo-fixer.js.map +1 -0
  31. package/dist/classification/certification-engine.js +208 -0
  32. package/dist/classification/certification-engine.js.map +1 -0
  33. package/dist/classification/feedback-collector.js +516 -0
  34. package/dist/classification/feedback-collector.js.map +1 -0
  35. package/dist/classification/l5-parameter-analyzer.js +670 -0
  36. package/dist/classification/l5-parameter-analyzer.js.map +1 -0
  37. package/dist/classification/l6-graph-analyzer.js +613 -0
  38. package/dist/classification/l6-graph-analyzer.js.map +1 -0
  39. package/dist/classification/severity-classifier.js +237 -0
  40. package/dist/classification/severity-classifier.js.map +1 -0
  41. package/dist/config/env.js +280 -0
  42. package/dist/config/env.js.map +1 -0
  43. package/dist/config/env.schema.js +234 -0
  44. package/dist/config/env.schema.js.map +1 -0
  45. package/dist/config/scraperEnv.js +55 -0
  46. package/dist/config/scraperEnv.js.map +1 -0
  47. package/dist/db/postgresClient.js +38 -0
  48. package/dist/db/postgresClient.js.map +1 -0
  49. package/dist/db/scraperDb.js +6 -0
  50. package/dist/db/scraperDb.js.map +1 -0
  51. package/dist/db/scraperPostgresClient.js +118 -0
  52. package/dist/db/scraperPostgresClient.js.map +1 -0
  53. package/dist/db/validationRepository.js +55 -0
  54. package/dist/db/validationRepository.js.map +1 -0
  55. package/dist/db/validatorPostgresClient.js +248 -0
  56. package/dist/db/validatorPostgresClient.js.map +1 -0
  57. package/dist/db/workflowInstanceMappingRepository.js +128 -0
  58. package/dist/db/workflowInstanceMappingRepository.js.map +1 -0
  59. package/dist/errors/AppError.js +156 -0
  60. package/dist/errors/AppError.js.map +1 -0
  61. package/dist/errors/index.js +7 -0
  62. package/dist/errors/index.js.map +1 -0
  63. package/dist/factory/error-to-problem-mappers.js +385 -0
  64. package/dist/factory/error-to-problem-mappers.js.map +1 -0
  65. package/dist/factory/gap-recorder.js +260 -0
  66. package/dist/factory/gap-recorder.js.map +1 -0
  67. package/dist/factory/problem-recorder.js +94 -0
  68. package/dist/factory/problem-recorder.js.map +1 -0
  69. package/dist/factory/warning-to-gap-mappers.js +493 -0
  70. package/dist/factory/warning-to-gap-mappers.js.map +1 -0
  71. package/dist/factory/workflow-normalizer.js +247 -0
  72. package/dist/factory/workflow-normalizer.js.map +1 -0
  73. package/dist/mcp/adapters/catalog.js +13 -0
  74. package/dist/mcp/adapters/catalog.js.map +1 -0
  75. package/dist/mcp/adapters/index.js +36 -0
  76. package/dist/mcp/adapters/index.js.map +1 -0
  77. package/dist/mcp/adapters/supabase-catalog.js +467 -0
  78. package/dist/mcp/adapters/supabase-catalog.js.map +1 -0
  79. package/dist/mcp/adapters/test-catalog-adapter.js +100 -0
  80. package/dist/mcp/adapters/test-catalog-adapter.js.map +1 -0
  81. package/dist/mcp/adapters/validation.js +258 -0
  82. package/dist/mcp/adapters/validation.js.map +1 -0
  83. package/dist/mcp/build-email-workflow.js +113 -0
  84. package/dist/mcp/build-email-workflow.js.map +1 -0
  85. package/dist/mcp/config.js +22 -0
  86. package/dist/mcp/config.js.map +1 -0
  87. package/dist/mcp/formatters/errors.js +217 -0
  88. package/dist/mcp/formatters/errors.js.map +1 -0
  89. package/dist/mcp/formatters/index.js +12 -0
  90. package/dist/mcp/formatters/index.js.map +1 -0
  91. package/dist/mcp/formatters/response.js +141 -0
  92. package/dist/mcp/formatters/response.js.map +1 -0
  93. package/dist/mcp/quick-test.js +33 -0
  94. package/dist/mcp/quick-test.js.map +1 -0
  95. package/dist/mcp/server.js +70 -0
  96. package/dist/mcp/server.js.map +1 -0
  97. package/dist/mcp/test-mcp-error.js +81 -0
  98. package/dist/mcp/test-mcp-error.js.map +1 -0
  99. package/dist/mcp/test-mcp.js +80 -0
  100. package/dist/mcp/test-mcp.js.map +1 -0
  101. package/dist/mcp/tools/fixes/expression-fixes.js +166 -0
  102. package/dist/mcp/tools/fixes/expression-fixes.js.map +1 -0
  103. package/dist/mcp/tools/fixes/flow-fixes.js +155 -0
  104. package/dist/mcp/tools/fixes/flow-fixes.js.map +1 -0
  105. package/dist/mcp/tools/fixes/index.js +91 -0
  106. package/dist/mcp/tools/fixes/index.js.map +1 -0
  107. package/dist/mcp/tools/fixes/node-fixes.js +233 -0
  108. package/dist/mcp/tools/fixes/node-fixes.js.map +1 -0
  109. package/dist/mcp/tools/fixes/parameter-fixes.js +277 -0
  110. package/dist/mcp/tools/fixes/parameter-fixes.js.map +1 -0
  111. package/dist/mcp/tools/fixes/types.js +10 -0
  112. package/dist/mcp/tools/fixes/types.js.map +1 -0
  113. package/dist/mcp/tools/handlers/check-parameter.js +300 -0
  114. package/dist/mcp/tools/handlers/check-parameter.js.map +1 -0
  115. package/dist/mcp/tools/handlers/find-similar-pattern.js +121 -0
  116. package/dist/mcp/tools/handlers/find-similar-pattern.js.map +1 -0
  117. package/dist/mcp/tools/handlers/get-node-info.js +131 -0
  118. package/dist/mcp/tools/handlers/get-node-info.js.map +1 -0
  119. package/dist/mcp/tools/handlers/get-operation-schema.js +141 -0
  120. package/dist/mcp/tools/handlers/get-operation-schema.js.map +1 -0
  121. package/dist/mcp/tools/handlers/list-nodes.js +126 -0
  122. package/dist/mcp/tools/handlers/list-nodes.js.map +1 -0
  123. package/dist/mcp/tools/handlers/list-operations.js +138 -0
  124. package/dist/mcp/tools/handlers/list-operations.js.map +1 -0
  125. package/dist/mcp/tools/handlers/suggest-fix.js +120 -0
  126. package/dist/mcp/tools/handlers/suggest-fix.js.map +1 -0
  127. package/dist/mcp/tools/handlers/validate-workflow.js +92 -0
  128. package/dist/mcp/tools/handlers/validate-workflow.js.map +1 -0
  129. package/dist/mcp/tools/index.js +190 -0
  130. package/dist/mcp/tools/index.js.map +1 -0
  131. package/dist/mcp/tools/schemas.js +195 -0
  132. package/dist/mcp/tools/schemas.js.map +1 -0
  133. package/dist/mcp/tools/validate.js +95 -0
  134. package/dist/mcp/tools/validate.js.map +1 -0
  135. package/dist/mcp/types/mcp.js +7 -0
  136. package/dist/mcp/types/mcp.js.map +1 -0
  137. package/dist/mcp/utils/timeout.js +78 -0
  138. package/dist/mcp/utils/timeout.js.map +1 -0
  139. package/dist/services/BatchProcessor.js +433 -0
  140. package/dist/services/BatchProcessor.js.map +1 -0
  141. package/dist/services/CheckpointManager.js +281 -0
  142. package/dist/services/CheckpointManager.js.map +1 -0
  143. package/dist/services/CostCalculator.js +211 -0
  144. package/dist/services/CostCalculator.js.map +1 -0
  145. package/dist/services/EmbeddingCache.js +68 -0
  146. package/dist/services/EmbeddingCache.js.map +1 -0
  147. package/dist/services/EmbeddingService.js +143 -0
  148. package/dist/services/EmbeddingService.js.map +1 -0
  149. package/dist/services/RankingService.js +81 -0
  150. package/dist/services/RankingService.js.map +1 -0
  151. package/dist/services/RedisCache.js +376 -0
  152. package/dist/services/RedisCache.js.map +1 -0
  153. package/dist/services/RedisCatalogCache.js +680 -0
  154. package/dist/services/RedisCatalogCache.js.map +1 -0
  155. package/dist/services/ResumeManager.js +252 -0
  156. package/dist/services/ResumeManager.js.map +1 -0
  157. package/dist/services/SearchService.js +282 -0
  158. package/dist/services/SearchService.js.map +1 -0
  159. package/dist/services/SemanticCatalogSearch.js +405 -0
  160. package/dist/services/SemanticCatalogSearch.js.map +1 -0
  161. package/dist/services/ValidationCache.js +157 -0
  162. package/dist/services/ValidationCache.js.map +1 -0
  163. package/dist/services/WorkflowPipelineService.js +1997 -0
  164. package/dist/services/WorkflowPipelineService.js.map +1 -0
  165. package/dist/services/catalog/index.js +34 -0
  166. package/dist/services/catalog/index.js.map +1 -0
  167. package/dist/services/catalog/interfaces.js +17 -0
  168. package/dist/services/catalog/interfaces.js.map +1 -0
  169. package/dist/services/catalog/loaders.js +169 -0
  170. package/dist/services/catalog/loaders.js.map +1 -0
  171. package/dist/services/catalog/types.js +138 -0
  172. package/dist/services/catalog/types.js.map +1 -0
  173. package/dist/services/documentation-normalization/docUrlUtils.js +88 -0
  174. package/dist/services/documentation-normalization/docUrlUtils.js.map +1 -0
  175. package/dist/services/error-quality/ErrorQualityService.js +262 -0
  176. package/dist/services/error-quality/ErrorQualityService.js.map +1 -0
  177. package/dist/services/error-quality/analyzers/CredentialAnalyzer.js +260 -0
  178. package/dist/services/error-quality/analyzers/CredentialAnalyzer.js.map +1 -0
  179. package/dist/services/error-quality/analyzers/IssuePredictor.js +380 -0
  180. package/dist/services/error-quality/analyzers/IssuePredictor.js.map +1 -0
  181. package/dist/services/error-quality/analyzers/MockCoverageAnalyzer.js +267 -0
  182. package/dist/services/error-quality/analyzers/MockCoverageAnalyzer.js.map +1 -0
  183. package/dist/services/error-quality/data/ErrorPatternSeeder.js +963 -0
  184. package/dist/services/error-quality/data/ErrorPatternSeeder.js.map +1 -0
  185. package/dist/services/error-quality/index.js +25 -0
  186. package/dist/services/error-quality/index.js.map +1 -0
  187. package/dist/services/error-quality/reports/ReportGenerator.js +343 -0
  188. package/dist/services/error-quality/reports/ReportGenerator.js.map +1 -0
  189. package/dist/services/error-quality/taxonomy/ErrorTaxonomy.js +698 -0
  190. package/dist/services/error-quality/taxonomy/ErrorTaxonomy.js.map +1 -0
  191. package/dist/services/error-quality/types.js +11 -0
  192. package/dist/services/error-quality/types.js.map +1 -0
  193. package/dist/services/progress/ProgressTracker.js +288 -0
  194. package/dist/services/progress/ProgressTracker.js.map +1 -0
  195. package/dist/services/progress/formatters.js +122 -0
  196. package/dist/services/progress/formatters.js.map +1 -0
  197. package/dist/services/progress/index.js +36 -0
  198. package/dist/services/progress/index.js.map +1 -0
  199. package/dist/services/progress/types.js +7 -0
  200. package/dist/services/progress/types.js.map +1 -0
  201. package/dist/services/search/embeddingGenerator.js +112 -0
  202. package/dist/services/search/embeddingGenerator.js.map +1 -0
  203. package/dist/types/aiCapabilities.js +7 -0
  204. package/dist/types/aiCapabilities.js.map +1 -0
  205. package/dist/types/aiConfigSchema.js +7 -0
  206. package/dist/types/aiConfigSchema.js.map +1 -0
  207. package/dist/utils/bannerLogger.js +186 -0
  208. package/dist/utils/bannerLogger.js.map +1 -0
  209. package/dist/utils/bannerService.js +23 -0
  210. package/dist/utils/bannerService.js.map +1 -0
  211. package/dist/utils/bannerServiceAdapter.js +54 -0
  212. package/dist/utils/bannerServiceAdapter.js.map +1 -0
  213. package/dist/utils/batchLogger.js +171 -0
  214. package/dist/utils/batchLogger.js.map +1 -0
  215. package/dist/utils/bottomStickyBanner.js +239 -0
  216. package/dist/utils/bottomStickyBanner.js.map +1 -0
  217. package/dist/utils/credentialMatcher.js +206 -0
  218. package/dist/utils/credentialMatcher.js.map +1 -0
  219. package/dist/utils/credentialNormalizer.js +442 -0
  220. package/dist/utils/credentialNormalizer.js.map +1 -0
  221. package/dist/utils/integratedBannerLogger.js +59 -0
  222. package/dist/utils/integratedBannerLogger.js.map +1 -0
  223. package/dist/utils/n8nSourceGit.js +195 -0
  224. package/dist/utils/n8nSourceGit.js.map +1 -0
  225. package/dist/utils/nodeTypeNormalizer.js +131 -0
  226. package/dist/utils/nodeTypeNormalizer.js.map +1 -0
  227. package/dist/utils/openaiClient.js +397 -0
  228. package/dist/utils/openaiClient.js.map +1 -0
  229. package/dist/utils/productionLogger.js +16 -0
  230. package/dist/utils/productionLogger.js.map +1 -0
  231. package/dist/utils/progressBarBanner.js +132 -0
  232. package/dist/utils/progressBarBanner.js.map +1 -0
  233. package/dist/utils/scriptHeartbeat.js +117 -0
  234. package/dist/utils/scriptHeartbeat.js.map +1 -0
  235. package/dist/utils/scriptLogger.js +125 -0
  236. package/dist/utils/scriptLogger.js.map +1 -0
  237. package/dist/utils/scriptRunner.js +95 -0
  238. package/dist/utils/scriptRunner.js.map +1 -0
  239. package/dist/utils/scriptTimeout.js +128 -0
  240. package/dist/utils/scriptTimeout.js.map +1 -0
  241. package/dist/utils/scriptWrapper.js +219 -0
  242. package/dist/utils/scriptWrapper.js.map +1 -0
  243. package/dist/utils/stickyBanner.js +226 -0
  244. package/dist/utils/stickyBanner.js.map +1 -0
  245. package/dist/utils/terminalSpinner.js +97 -0
  246. package/dist/utils/terminalSpinner.js.map +1 -0
  247. package/dist/utils/threeLineBanner.js +427 -0
  248. package/dist/utils/threeLineBanner.js.map +1 -0
  249. package/dist/utils/validatorCheckpointManager.js +170 -0
  250. package/dist/utils/validatorCheckpointManager.js.map +1 -0
  251. package/dist/utils/validatorConnectionManager.js +124 -0
  252. package/dist/utils/validatorConnectionManager.js.map +1 -0
  253. package/dist/validation/catalog.js +56 -0
  254. package/dist/validation/catalog.js.map +1 -0
  255. package/dist/validation/config/deprecated-nodes.js +234 -0
  256. package/dist/validation/config/deprecated-nodes.js.map +1 -0
  257. package/dist/validation/config/l6-severity.js +227 -0
  258. package/dist/validation/config/l6-severity.js.map +1 -0
  259. package/dist/validation/config/terminal-nodes.js +132 -0
  260. package/dist/validation/config/terminal-nodes.js.map +1 -0
  261. package/dist/validation/config/unreachable-nodes.js +67 -0
  262. package/dist/validation/config/unreachable-nodes.js.map +1 -0
  263. package/dist/validation/core.js +47 -0
  264. package/dist/validation/core.js.map +1 -0
  265. package/dist/validation/docExtraction.js +12 -0
  266. package/dist/validation/docExtraction.js.map +1 -0
  267. package/dist/validation/dryRunMockRunner.js +128 -0
  268. package/dist/validation/dryRunMockRunner.js.map +1 -0
  269. package/dist/validation/fixtureEngine.js +61 -0
  270. package/dist/validation/fixtureEngine.js.map +1 -0
  271. package/dist/validation/index.js +15 -0
  272. package/dist/validation/index.js.map +1 -0
  273. package/dist/validation/k-levels/k2-blockers.js +222 -0
  274. package/dist/validation/k-levels/k2-blockers.js.map +1 -0
  275. package/dist/validation/l1-structure.js +296 -0
  276. package/dist/validation/l1-structure.js.map +1 -0
  277. package/dist/validation/l2-nodes.js +282 -0
  278. package/dist/validation/l2-nodes.js.map +1 -0
  279. package/dist/validation/l3-credentials.js +322 -0
  280. package/dist/validation/l3-credentials.js.map +1 -0
  281. package/dist/validation/l4-connections.js +698 -0
  282. package/dist/validation/l4-connections.js.map +1 -0
  283. package/dist/validation/l5-parameters.js +803 -0
  284. package/dist/validation/l5-parameters.js.map +1 -0
  285. package/dist/validation/l6-checks/ai-tool-variants.js +407 -0
  286. package/dist/validation/l6-checks/ai-tool-variants.js.map +1 -0
  287. package/dist/validation/l6-checks/catalog-checks.js +260 -0
  288. package/dist/validation/l6-checks/catalog-checks.js.map +1 -0
  289. package/dist/validation/l6-checks/data-contracts.js +197 -0
  290. package/dist/validation/l6-checks/data-contracts.js.map +1 -0
  291. package/dist/validation/l6-checks/deprecation.js +133 -0
  292. package/dist/validation/l6-checks/deprecation.js.map +1 -0
  293. package/dist/validation/l6-checks/error-handling.js +193 -0
  294. package/dist/validation/l6-checks/error-handling.js.map +1 -0
  295. package/dist/validation/l6-checks/expression-syntax.js +387 -0
  296. package/dist/validation/l6-checks/expression-syntax.js.map +1 -0
  297. package/dist/validation/l6-checks/flow-integrity.js +504 -0
  298. package/dist/validation/l6-checks/flow-integrity.js.map +1 -0
  299. package/dist/validation/l6-checks/index.js +106 -0
  300. package/dist/validation/l6-checks/index.js.map +1 -0
  301. package/dist/validation/l6-checks/loops.js +370 -0
  302. package/dist/validation/l6-checks/loops.js.map +1 -0
  303. package/dist/validation/l6-checks/performance.js +182 -0
  304. package/dist/validation/l6-checks/performance.js.map +1 -0
  305. package/dist/validation/l6-checks/security.js +273 -0
  306. package/dist/validation/l6-checks/security.js.map +1 -0
  307. package/dist/validation/l6-patterns.js +472 -0
  308. package/dist/validation/l6-patterns.js.map +1 -0
  309. package/dist/validation/mockLevelResolver.js +95 -0
  310. package/dist/validation/mockLevelResolver.js.map +1 -0
  311. package/dist/validation/n8nApiClient.js +21 -0
  312. package/dist/validation/n8nApiClient.js.map +1 -0
  313. package/dist/validation/n8nCli.js +87 -0
  314. package/dist/validation/n8nCli.js.map +1 -0
  315. package/dist/validation/types.js +8 -0
  316. package/dist/validation/types.js.map +1 -0
  317. package/dist/validation/usageStats.js +82 -0
  318. package/dist/validation/usageStats.js.map +1 -0
  319. 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