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