@synergenius/flow-weaver 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/LICENSE +122 -0
  2. package/README.md +315 -0
  3. package/dist/annotation-generator.d.ts +45 -0
  4. package/dist/annotation-generator.js +557 -0
  5. package/dist/api/builder.d.ts +223 -0
  6. package/dist/api/builder.js +345 -0
  7. package/dist/api/compile.d.ts +92 -0
  8. package/dist/api/compile.js +149 -0
  9. package/dist/api/extract-types.d.ts +29 -0
  10. package/dist/api/extract-types.js +57 -0
  11. package/dist/api/generate-in-place.d.ts +73 -0
  12. package/dist/api/generate-in-place.js +1353 -0
  13. package/dist/api/generate.d.ts +83 -0
  14. package/dist/api/generate.js +510 -0
  15. package/dist/api/helpers.d.ts +248 -0
  16. package/dist/api/helpers.js +285 -0
  17. package/dist/api/index.d.ts +46 -0
  18. package/dist/api/index.js +45 -0
  19. package/dist/api/inline-runtime.d.ts +27 -0
  20. package/dist/api/inline-runtime.js +551 -0
  21. package/dist/api/manipulation/connections.d.ts +79 -0
  22. package/dist/api/manipulation/connections.js +151 -0
  23. package/dist/api/manipulation/index.d.ts +34 -0
  24. package/dist/api/manipulation/index.js +41 -0
  25. package/dist/api/manipulation/node-types.d.ts +123 -0
  26. package/dist/api/manipulation/node-types.js +200 -0
  27. package/dist/api/manipulation/nodes.d.ts +144 -0
  28. package/dist/api/manipulation/nodes.js +333 -0
  29. package/dist/api/manipulation/ports.d.ts +59 -0
  30. package/dist/api/manipulation/ports.js +228 -0
  31. package/dist/api/manipulation/scopes.d.ts +52 -0
  32. package/dist/api/manipulation/scopes.js +156 -0
  33. package/dist/api/manipulation/validation.d.ts +6 -0
  34. package/dist/api/manipulation/validation.js +6 -0
  35. package/dist/api/manipulation/workflow.d.ts +81 -0
  36. package/dist/api/manipulation/workflow.js +116 -0
  37. package/dist/api/manipulation.d.ts +8 -0
  38. package/dist/api/manipulation.js +8 -0
  39. package/dist/api/parse.d.ts +48 -0
  40. package/dist/api/parse.js +110 -0
  41. package/dist/api/patterns.d.ts +112 -0
  42. package/dist/api/patterns.js +306 -0
  43. package/dist/api/query.d.ts +429 -0
  44. package/dist/api/query.js +816 -0
  45. package/dist/api/templates.d.ts +98 -0
  46. package/dist/api/templates.js +117 -0
  47. package/dist/api/transform.d.ts +31 -0
  48. package/dist/api/transform.js +40 -0
  49. package/dist/api/validate.d.ts +25 -0
  50. package/dist/api/validate.js +39 -0
  51. package/dist/api/workflow-file-operations.d.ts +29 -0
  52. package/dist/api/workflow-file-operations.js +180 -0
  53. package/dist/ast/builder.d.ts +210 -0
  54. package/dist/ast/builder.js +395 -0
  55. package/dist/ast/index.d.ts +5 -0
  56. package/dist/ast/index.js +5 -0
  57. package/dist/ast/serialization-node.d.ts +6 -0
  58. package/dist/ast/serialization-node.js +30 -0
  59. package/dist/ast/serialization.d.ts +43 -0
  60. package/dist/ast/serialization.js +134 -0
  61. package/dist/ast/types.d.ts +852 -0
  62. package/dist/ast/types.js +2 -0
  63. package/dist/ast/workflow-utils.d.ts +54 -0
  64. package/dist/ast/workflow-utils.js +114 -0
  65. package/dist/body-generator.d.ts +31 -0
  66. package/dist/body-generator.js +35 -0
  67. package/dist/built-in-nodes/delay.d.ts +11 -0
  68. package/dist/built-in-nodes/delay.js +29 -0
  69. package/dist/built-in-nodes/index.d.ts +5 -0
  70. package/dist/built-in-nodes/index.js +4 -0
  71. package/dist/built-in-nodes/invoke-workflow.d.ts +13 -0
  72. package/dist/built-in-nodes/invoke-workflow.js +25 -0
  73. package/dist/built-in-nodes/mock-types.d.ts +18 -0
  74. package/dist/built-in-nodes/mock-types.js +12 -0
  75. package/dist/built-in-nodes/wait-for-event.d.ts +13 -0
  76. package/dist/built-in-nodes/wait-for-event.js +25 -0
  77. package/dist/chevrotain-parser/connect-parser.d.ts +24 -0
  78. package/dist/chevrotain-parser/connect-parser.js +98 -0
  79. package/dist/chevrotain-parser/grammar-diagrams.d.ts +29 -0
  80. package/dist/chevrotain-parser/grammar-diagrams.js +264 -0
  81. package/dist/chevrotain-parser/index.d.ts +25 -0
  82. package/dist/chevrotain-parser/index.js +27 -0
  83. package/dist/chevrotain-parser/map-parser.d.ts +33 -0
  84. package/dist/chevrotain-parser/map-parser.js +130 -0
  85. package/dist/chevrotain-parser/node-parser.d.ts +36 -0
  86. package/dist/chevrotain-parser/node-parser.js +466 -0
  87. package/dist/chevrotain-parser/path-parser.d.ts +28 -0
  88. package/dist/chevrotain-parser/path-parser.js +118 -0
  89. package/dist/chevrotain-parser/port-parser.d.ts +36 -0
  90. package/dist/chevrotain-parser/port-parser.js +442 -0
  91. package/dist/chevrotain-parser/position-parser.d.ts +20 -0
  92. package/dist/chevrotain-parser/position-parser.js +83 -0
  93. package/dist/chevrotain-parser/scope-parser.d.ts +19 -0
  94. package/dist/chevrotain-parser/scope-parser.js +104 -0
  95. package/dist/chevrotain-parser/tokens.d.ts +78 -0
  96. package/dist/chevrotain-parser/tokens.js +384 -0
  97. package/dist/chevrotain-parser/trigger-cancel-parser.d.ts +50 -0
  98. package/dist/chevrotain-parser/trigger-cancel-parser.js +282 -0
  99. package/dist/cli/commands/changelog.d.ts +13 -0
  100. package/dist/cli/commands/changelog.js +135 -0
  101. package/dist/cli/commands/compile.d.ts +64 -0
  102. package/dist/cli/commands/compile.js +278 -0
  103. package/dist/cli/commands/create.d.ts +33 -0
  104. package/dist/cli/commands/create.js +147 -0
  105. package/dist/cli/commands/describe.d.ts +68 -0
  106. package/dist/cli/commands/describe.js +377 -0
  107. package/dist/cli/commands/dev.d.ts +32 -0
  108. package/dist/cli/commands/dev.js +384 -0
  109. package/dist/cli/commands/diagram.d.ts +13 -0
  110. package/dist/cli/commands/diagram.js +33 -0
  111. package/dist/cli/commands/diff.d.ts +11 -0
  112. package/dist/cli/commands/diff.js +59 -0
  113. package/dist/cli/commands/doctor.d.ts +57 -0
  114. package/dist/cli/commands/doctor.js +719 -0
  115. package/dist/cli/commands/export.d.ts +57 -0
  116. package/dist/cli/commands/export.js +163 -0
  117. package/dist/cli/commands/grammar.d.ts +9 -0
  118. package/dist/cli/commands/grammar.js +39 -0
  119. package/dist/cli/commands/init.d.ts +59 -0
  120. package/dist/cli/commands/init.js +435 -0
  121. package/dist/cli/commands/listen.d.ts +16 -0
  122. package/dist/cli/commands/listen.js +39 -0
  123. package/dist/cli/commands/market.d.ts +52 -0
  124. package/dist/cli/commands/market.js +436 -0
  125. package/dist/cli/commands/migrate.d.ts +13 -0
  126. package/dist/cli/commands/migrate.js +89 -0
  127. package/dist/cli/commands/openapi.d.ts +37 -0
  128. package/dist/cli/commands/openapi.js +67 -0
  129. package/dist/cli/commands/pattern.d.ts +34 -0
  130. package/dist/cli/commands/pattern.js +185 -0
  131. package/dist/cli/commands/plugin.d.ts +16 -0
  132. package/dist/cli/commands/plugin.js +176 -0
  133. package/dist/cli/commands/run.d.ts +49 -0
  134. package/dist/cli/commands/run.js +191 -0
  135. package/dist/cli/commands/serve.d.ts +45 -0
  136. package/dist/cli/commands/serve.js +81 -0
  137. package/dist/cli/commands/templates.d.ts +8 -0
  138. package/dist/cli/commands/templates.js +54 -0
  139. package/dist/cli/commands/ui.d.ts +16 -0
  140. package/dist/cli/commands/ui.js +130 -0
  141. package/dist/cli/commands/validate.d.ts +12 -0
  142. package/dist/cli/commands/validate.js +247 -0
  143. package/dist/cli/commands/watch.d.ts +9 -0
  144. package/dist/cli/commands/watch.js +70 -0
  145. package/dist/cli/flow-weaver.mjs +92924 -0
  146. package/dist/cli/index.d.ts +9 -0
  147. package/dist/cli/index.js +742 -0
  148. package/dist/cli/templates/ai/mock-provider.d.ts +7 -0
  149. package/dist/cli/templates/ai/mock-provider.js +64 -0
  150. package/dist/cli/templates/ai/types.d.ts +47 -0
  151. package/dist/cli/templates/ai/types.js +5 -0
  152. package/dist/cli/templates/approvals/index.d.ts +15 -0
  153. package/dist/cli/templates/approvals/index.js +241 -0
  154. package/dist/cli/templates/index.d.ts +102 -0
  155. package/dist/cli/templates/index.js +101 -0
  156. package/dist/cli/templates/nodes/agent-router.d.ts +3 -0
  157. package/dist/cli/templates/nodes/agent-router.js +114 -0
  158. package/dist/cli/templates/nodes/aggregator.d.ts +7 -0
  159. package/dist/cli/templates/nodes/aggregator.js +63 -0
  160. package/dist/cli/templates/nodes/conversation-memory.d.ts +3 -0
  161. package/dist/cli/templates/nodes/conversation-memory.js +85 -0
  162. package/dist/cli/templates/nodes/http.d.ts +7 -0
  163. package/dist/cli/templates/nodes/http.js +80 -0
  164. package/dist/cli/templates/nodes/human-approval.d.ts +3 -0
  165. package/dist/cli/templates/nodes/human-approval.js +110 -0
  166. package/dist/cli/templates/nodes/json-extractor.d.ts +3 -0
  167. package/dist/cli/templates/nodes/json-extractor.js +119 -0
  168. package/dist/cli/templates/nodes/llm-call.d.ts +3 -0
  169. package/dist/cli/templates/nodes/llm-call.js +106 -0
  170. package/dist/cli/templates/nodes/prompt-template.d.ts +3 -0
  171. package/dist/cli/templates/nodes/prompt-template.js +52 -0
  172. package/dist/cli/templates/nodes/rag-retriever.d.ts +3 -0
  173. package/dist/cli/templates/nodes/rag-retriever.js +128 -0
  174. package/dist/cli/templates/nodes/tool-executor.d.ts +3 -0
  175. package/dist/cli/templates/nodes/tool-executor.js +108 -0
  176. package/dist/cli/templates/nodes/transformer.d.ts +7 -0
  177. package/dist/cli/templates/nodes/transformer.js +68 -0
  178. package/dist/cli/templates/nodes/validator.d.ts +7 -0
  179. package/dist/cli/templates/nodes/validator.js +62 -0
  180. package/dist/cli/templates/providers/index.d.ts +14 -0
  181. package/dist/cli/templates/providers/index.js +239 -0
  182. package/dist/cli/templates/shared/approval-types.d.ts +9 -0
  183. package/dist/cli/templates/shared/approval-types.js +31 -0
  184. package/dist/cli/templates/shared/llm-types.d.ts +15 -0
  185. package/dist/cli/templates/shared/llm-types.js +104 -0
  186. package/dist/cli/templates/workflows/aggregator.d.ts +7 -0
  187. package/dist/cli/templates/workflows/aggregator.js +104 -0
  188. package/dist/cli/templates/workflows/ai-agent-durable.d.ts +8 -0
  189. package/dist/cli/templates/workflows/ai-agent-durable.js +338 -0
  190. package/dist/cli/templates/workflows/ai-agent.d.ts +31 -0
  191. package/dist/cli/templates/workflows/ai-agent.js +326 -0
  192. package/dist/cli/templates/workflows/ai-chat.d.ts +7 -0
  193. package/dist/cli/templates/workflows/ai-chat.js +169 -0
  194. package/dist/cli/templates/workflows/ai-pipeline-durable.d.ts +8 -0
  195. package/dist/cli/templates/workflows/ai-pipeline-durable.js +330 -0
  196. package/dist/cli/templates/workflows/ai-rag.d.ts +7 -0
  197. package/dist/cli/templates/workflows/ai-rag.js +186 -0
  198. package/dist/cli/templates/workflows/ai-react.d.ts +7 -0
  199. package/dist/cli/templates/workflows/ai-react.js +294 -0
  200. package/dist/cli/templates/workflows/conditional.d.ts +12 -0
  201. package/dist/cli/templates/workflows/conditional.js +142 -0
  202. package/dist/cli/templates/workflows/error-handler.d.ts +7 -0
  203. package/dist/cli/templates/workflows/error-handler.js +147 -0
  204. package/dist/cli/templates/workflows/foreach.d.ts +7 -0
  205. package/dist/cli/templates/workflows/foreach.js +143 -0
  206. package/dist/cli/templates/workflows/sequential.d.ts +7 -0
  207. package/dist/cli/templates/workflows/sequential.js +198 -0
  208. package/dist/cli/templates/workflows/webhook.d.ts +7 -0
  209. package/dist/cli/templates/workflows/webhook.js +161 -0
  210. package/dist/cli/utils/logger.d.ts +15 -0
  211. package/dist/cli/utils/logger.js +46 -0
  212. package/dist/constants.d.ts +100 -0
  213. package/dist/constants.js +125 -0
  214. package/dist/defaults.d.ts +3 -0
  215. package/dist/defaults.js +3 -0
  216. package/dist/deployment/config/defaults.d.ts +29 -0
  217. package/dist/deployment/config/defaults.js +98 -0
  218. package/dist/deployment/config/loader.d.ts +24 -0
  219. package/dist/deployment/config/loader.js +236 -0
  220. package/dist/deployment/config/types.d.ts +117 -0
  221. package/dist/deployment/config/types.js +5 -0
  222. package/dist/deployment/core/adapters.d.ts +90 -0
  223. package/dist/deployment/core/adapters.js +251 -0
  224. package/dist/deployment/core/executor.d.ts +62 -0
  225. package/dist/deployment/core/executor.js +197 -0
  226. package/dist/deployment/core/formatters.d.ts +57 -0
  227. package/dist/deployment/core/formatters.js +170 -0
  228. package/dist/deployment/index.d.ts +31 -0
  229. package/dist/deployment/index.js +48 -0
  230. package/dist/deployment/openapi/generator.d.ts +146 -0
  231. package/dist/deployment/openapi/generator.js +347 -0
  232. package/dist/deployment/openapi/schema-converter.d.ts +49 -0
  233. package/dist/deployment/openapi/schema-converter.js +192 -0
  234. package/dist/deployment/targets/base.d.ts +316 -0
  235. package/dist/deployment/targets/base.js +823 -0
  236. package/dist/deployment/targets/cloudflare.d.ts +23 -0
  237. package/dist/deployment/targets/cloudflare.js +1125 -0
  238. package/dist/deployment/targets/inngest.d.ts +38 -0
  239. package/dist/deployment/targets/inngest.js +926 -0
  240. package/dist/deployment/targets/lambda.d.ts +23 -0
  241. package/dist/deployment/targets/lambda.js +1289 -0
  242. package/dist/deployment/targets/vercel.d.ts +23 -0
  243. package/dist/deployment/targets/vercel.js +886 -0
  244. package/dist/deployment/types.d.ts +183 -0
  245. package/dist/deployment/types.js +8 -0
  246. package/dist/diagram/geometry.d.ts +26 -0
  247. package/dist/diagram/geometry.js +850 -0
  248. package/dist/diagram/index.d.ts +16 -0
  249. package/dist/diagram/index.js +42 -0
  250. package/dist/diagram/layout.d.ts +11 -0
  251. package/dist/diagram/layout.js +143 -0
  252. package/dist/diagram/orthogonal-router.d.ts +79 -0
  253. package/dist/diagram/orthogonal-router.js +568 -0
  254. package/dist/diagram/renderer.d.ts +3 -0
  255. package/dist/diagram/renderer.js +207 -0
  256. package/dist/diagram/theme.d.ts +20 -0
  257. package/dist/diagram/theme.js +189 -0
  258. package/dist/diagram/types.d.ts +70 -0
  259. package/dist/diagram/types.js +2 -0
  260. package/dist/diff/WorkflowDiffer.d.ts +13 -0
  261. package/dist/diff/WorkflowDiffer.js +429 -0
  262. package/dist/diff/formatDiff.d.ts +10 -0
  263. package/dist/diff/formatDiff.js +220 -0
  264. package/dist/diff/impact.d.ts +29 -0
  265. package/dist/diff/impact.js +119 -0
  266. package/dist/diff/index.d.ts +10 -0
  267. package/dist/diff/index.js +9 -0
  268. package/dist/diff/types.d.ts +138 -0
  269. package/dist/diff/types.js +35 -0
  270. package/dist/doc-metadata/extractors/annotations.d.ts +56 -0
  271. package/dist/doc-metadata/extractors/annotations.js +337 -0
  272. package/dist/doc-metadata/extractors/cli-commands.d.ts +17 -0
  273. package/dist/doc-metadata/extractors/cli-commands.js +355 -0
  274. package/dist/doc-metadata/extractors/mcp-tools.d.ts +16 -0
  275. package/dist/doc-metadata/extractors/mcp-tools.js +689 -0
  276. package/dist/doc-metadata/extractors/plugin-api.d.ts +19 -0
  277. package/dist/doc-metadata/extractors/plugin-api.js +279 -0
  278. package/dist/doc-metadata/index.d.ts +5 -0
  279. package/dist/doc-metadata/index.js +4 -0
  280. package/dist/doc-metadata/types.d.ts +120 -0
  281. package/dist/doc-metadata/types.js +5 -0
  282. package/dist/editor-completions/annotationValues.d.ts +12 -0
  283. package/dist/editor-completions/annotationValues.js +138 -0
  284. package/dist/editor-completions/contextParser.d.ts +40 -0
  285. package/dist/editor-completions/contextParser.js +410 -0
  286. package/dist/editor-completions/dataTypes.d.ts +16 -0
  287. package/dist/editor-completions/dataTypes.js +95 -0
  288. package/dist/editor-completions/goToDefinition.d.ts +27 -0
  289. package/dist/editor-completions/goToDefinition.js +112 -0
  290. package/dist/editor-completions/index.d.ts +39 -0
  291. package/dist/editor-completions/index.js +181 -0
  292. package/dist/editor-completions/jsDocAnnotations.d.ts +29 -0
  293. package/dist/editor-completions/jsDocAnnotations.js +357 -0
  294. package/dist/editor-completions/modifierCompletions.d.ts +17 -0
  295. package/dist/editor-completions/modifierCompletions.js +197 -0
  296. package/dist/editor-completions/types.d.ts +119 -0
  297. package/dist/editor-completions/types.js +8 -0
  298. package/dist/export/index.d.ts +68 -0
  299. package/dist/export/index.js +1074 -0
  300. package/dist/export/templates.d.ts +24 -0
  301. package/dist/export/templates.js +186 -0
  302. package/dist/friendly-errors.d.ts +35 -0
  303. package/dist/friendly-errors.js +375 -0
  304. package/dist/function-like.d.ts +38 -0
  305. package/dist/function-like.js +83 -0
  306. package/dist/generated-branding.d.ts +16 -0
  307. package/dist/generated-branding.js +22 -0
  308. package/dist/generator/async-detection.d.ts +27 -0
  309. package/dist/generator/async-detection.js +56 -0
  310. package/dist/generator/code-utils.d.ts +76 -0
  311. package/dist/generator/code-utils.js +410 -0
  312. package/dist/generator/control-flow.d.ts +54 -0
  313. package/dist/generator/control-flow.js +284 -0
  314. package/dist/generator/inngest.d.ts +53 -0
  315. package/dist/generator/inngest.js +1126 -0
  316. package/dist/generator/scope-function-generator.d.ts +78 -0
  317. package/dist/generator/scope-function-generator.js +360 -0
  318. package/dist/generator/unified.d.ts +42 -0
  319. package/dist/generator/unified.js +1504 -0
  320. package/dist/generator.d.ts +54 -0
  321. package/dist/generator.js +100 -0
  322. package/dist/index.d.ts +85 -0
  323. package/dist/index.js +89 -0
  324. package/dist/jsdoc-parser.d.ts +308 -0
  325. package/dist/jsdoc-parser.js +923 -0
  326. package/dist/jsdoc-port-sync/constants.d.ts +41 -0
  327. package/dist/jsdoc-port-sync/constants.js +103 -0
  328. package/dist/jsdoc-port-sync/diff.d.ts +76 -0
  329. package/dist/jsdoc-port-sync/diff.js +319 -0
  330. package/dist/jsdoc-port-sync/index.d.ts +42 -0
  331. package/dist/jsdoc-port-sync/index.js +45 -0
  332. package/dist/jsdoc-port-sync/port-parser.d.ts +68 -0
  333. package/dist/jsdoc-port-sync/port-parser.js +579 -0
  334. package/dist/jsdoc-port-sync/rename.d.ts +21 -0
  335. package/dist/jsdoc-port-sync/rename.js +256 -0
  336. package/dist/jsdoc-port-sync/signature-parser.d.ts +104 -0
  337. package/dist/jsdoc-port-sync/signature-parser.js +559 -0
  338. package/dist/jsdoc-port-sync/sync.d.ts +36 -0
  339. package/dist/jsdoc-port-sync/sync.js +644 -0
  340. package/dist/jsdoc-port-sync.d.ts +10 -0
  341. package/dist/jsdoc-port-sync.js +10 -0
  342. package/dist/marketplace/index.d.ts +11 -0
  343. package/dist/marketplace/index.js +10 -0
  344. package/dist/marketplace/manifest.d.ts +32 -0
  345. package/dist/marketplace/manifest.js +176 -0
  346. package/dist/marketplace/registry.d.ts +30 -0
  347. package/dist/marketplace/registry.js +100 -0
  348. package/dist/marketplace/types.d.ts +154 -0
  349. package/dist/marketplace/types.js +9 -0
  350. package/dist/marketplace/validator.d.ts +13 -0
  351. package/dist/marketplace/validator.js +131 -0
  352. package/dist/mcp/auto-registration.d.ts +3 -0
  353. package/dist/mcp/auto-registration.js +62 -0
  354. package/dist/mcp/editor-connection.d.ts +50 -0
  355. package/dist/mcp/editor-connection.js +125 -0
  356. package/dist/mcp/event-buffer.d.ts +62 -0
  357. package/dist/mcp/event-buffer.js +150 -0
  358. package/dist/mcp/index.d.ts +12 -0
  359. package/dist/mcp/index.js +11 -0
  360. package/dist/mcp/resources.d.ts +14 -0
  361. package/dist/mcp/resources.js +55 -0
  362. package/dist/mcp/response-utils.d.ts +63 -0
  363. package/dist/mcp/response-utils.js +89 -0
  364. package/dist/mcp/server.d.ts +4 -0
  365. package/dist/mcp/server.js +99 -0
  366. package/dist/mcp/tools-diagram.d.ts +8 -0
  367. package/dist/mcp/tools-diagram.js +53 -0
  368. package/dist/mcp/tools-editor.d.ts +5 -0
  369. package/dist/mcp/tools-editor.js +190 -0
  370. package/dist/mcp/tools-export.d.ts +9 -0
  371. package/dist/mcp/tools-export.js +180 -0
  372. package/dist/mcp/tools-marketplace.d.ts +9 -0
  373. package/dist/mcp/tools-marketplace.js +132 -0
  374. package/dist/mcp/tools-pattern.d.ts +3 -0
  375. package/dist/mcp/tools-pattern.js +783 -0
  376. package/dist/mcp/tools-query.d.ts +3 -0
  377. package/dist/mcp/tools-query.js +364 -0
  378. package/dist/mcp/tools-template.d.ts +10 -0
  379. package/dist/mcp/tools-template.js +119 -0
  380. package/dist/mcp/types.d.ts +70 -0
  381. package/dist/mcp/types.js +8 -0
  382. package/dist/mcp/workflow-executor.d.ts +47 -0
  383. package/dist/mcp/workflow-executor.js +133 -0
  384. package/dist/migration/registry.d.ts +30 -0
  385. package/dist/migration/registry.js +29 -0
  386. package/dist/node-types-generator.d.ts +49 -0
  387. package/dist/node-types-generator.js +139 -0
  388. package/dist/npm-packages.d.ts +56 -0
  389. package/dist/npm-packages.js +255 -0
  390. package/dist/parser.d.ts +204 -0
  391. package/dist/parser.js +2100 -0
  392. package/dist/plugin/PluginPanel.d.ts +12 -0
  393. package/dist/plugin/PluginPanel.js +5 -0
  394. package/dist/plugin/index.d.ts +13 -0
  395. package/dist/plugin/index.js +14 -0
  396. package/dist/plugin/types.d.ts +75 -0
  397. package/dist/plugin/types.js +8 -0
  398. package/dist/resolve-package-types.d.ts +17 -0
  399. package/dist/resolve-package-types.js +123 -0
  400. package/dist/runtime/CancellationError.d.ts +11 -0
  401. package/dist/runtime/CancellationError.js +20 -0
  402. package/dist/runtime/ExecutionContext.d.ts +146 -0
  403. package/dist/runtime/ExecutionContext.js +235 -0
  404. package/dist/runtime/builtin-functions.d.ts +8 -0
  405. package/dist/runtime/builtin-functions.js +549 -0
  406. package/dist/runtime/events.d.ts +50 -0
  407. package/dist/runtime/events.js +2 -0
  408. package/dist/runtime/function-registry.d.ts +59 -0
  409. package/dist/runtime/function-registry.js +66 -0
  410. package/dist/runtime/index.d.ts +7 -0
  411. package/dist/runtime/index.js +7 -0
  412. package/dist/runtime/parameter-resolver.d.ts +62 -0
  413. package/dist/runtime/parameter-resolver.js +113 -0
  414. package/dist/server/index.d.ts +7 -0
  415. package/dist/server/index.js +6 -0
  416. package/dist/server/types.d.ts +93 -0
  417. package/dist/server/types.js +5 -0
  418. package/dist/server/webhook-server.d.ts +50 -0
  419. package/dist/server/webhook-server.js +269 -0
  420. package/dist/server/workflow-registry.d.ts +61 -0
  421. package/dist/server/workflow-registry.js +202 -0
  422. package/dist/shared-project.d.ts +9 -0
  423. package/dist/shared-project.js +28 -0
  424. package/dist/sugar-optimizer.d.ts +40 -0
  425. package/dist/sugar-optimizer.js +387 -0
  426. package/dist/testing/assertions.d.ts +51 -0
  427. package/dist/testing/assertions.js +127 -0
  428. package/dist/testing/index.d.ts +30 -0
  429. package/dist/testing/index.js +24 -0
  430. package/dist/testing/mock-approval.d.ts +81 -0
  431. package/dist/testing/mock-approval.js +98 -0
  432. package/dist/testing/mock-llm.d.ts +124 -0
  433. package/dist/testing/mock-llm.js +119 -0
  434. package/dist/testing/recorder.d.ts +72 -0
  435. package/dist/testing/recorder.js +70 -0
  436. package/dist/testing/replayer.d.ts +56 -0
  437. package/dist/testing/replayer.js +143 -0
  438. package/dist/testing/token-tracker.d.ts +71 -0
  439. package/dist/testing/token-tracker.js +94 -0
  440. package/dist/type-checker.d.ts +42 -0
  441. package/dist/type-checker.js +190 -0
  442. package/dist/type-mappings.d.ts +29 -0
  443. package/dist/type-mappings.js +125 -0
  444. package/dist/types/branded-ports.d.ts +151 -0
  445. package/dist/types/branded-ports.js +121 -0
  446. package/dist/types/index.d.ts +5 -0
  447. package/dist/types/index.js +5 -0
  448. package/dist/types.d.ts +139 -0
  449. package/dist/types.js +15 -0
  450. package/dist/utils/error-utils.d.ts +15 -0
  451. package/dist/utils/error-utils.js +27 -0
  452. package/dist/utils/lru-cache.d.ts +15 -0
  453. package/dist/utils/lru-cache.js +40 -0
  454. package/dist/utils/port-ordering.d.ts +26 -0
  455. package/dist/utils/port-ordering.js +88 -0
  456. package/dist/utils/port-tag-utils.d.ts +23 -0
  457. package/dist/utils/port-tag-utils.js +41 -0
  458. package/dist/utils/string-distance.d.ts +14 -0
  459. package/dist/utils/string-distance.js +56 -0
  460. package/dist/validation/agent-detection.d.ts +33 -0
  461. package/dist/validation/agent-detection.js +115 -0
  462. package/dist/validation/agent-rules.d.ts +48 -0
  463. package/dist/validation/agent-rules.js +262 -0
  464. package/dist/validator.d.ts +92 -0
  465. package/dist/validator.js +970 -0
  466. package/package.json +109 -0
package/dist/parser.js ADDED
@@ -0,0 +1,2100 @@
1
+ import { extractFunctionLikes } from './function-like.js';
2
+ import * as path from 'node:path';
3
+ import * as fs from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+ import { jsdocParser } from './jsdoc-parser.js';
6
+ import { EXECUTION_STRATEGIES, RESERVED_PORT_NAMES, isControlFlowPort } from './constants.js';
7
+ import { getErrorMessage } from './utils/error-utils.js';
8
+ import { assignImplicitPortOrders } from './utils/port-ordering.js';
9
+ import { stripGeneratedSections, hasInPlaceMarkers } from './api/generate-in-place.js';
10
+ import { inferDataTypeFromTS } from './type-mappings.js';
11
+ import { generateJSDocPortTag } from './annotation-generator.js';
12
+ import { resolvePackageTypesPath } from './resolve-package-types.js';
13
+ import { getPackageExports } from './npm-packages.js';
14
+ import { getSharedProject } from './shared-project.js';
15
+ import { LRUCache } from './utils/lru-cache.js';
16
+ /**
17
+ * Convert a TExternalNodeType to a TNodeTypeAST with sensible defaults.
18
+ * Used to merge runtime-loaded node types into the parser's available types.
19
+ */
20
+ function externalToAST(ext) {
21
+ const inputs = {};
22
+ const outputs = {};
23
+ if (ext.ports) {
24
+ for (const port of ext.ports) {
25
+ const def = {
26
+ dataType: port.type || 'ANY',
27
+ ...(port.defaultLabel && { label: port.defaultLabel }),
28
+ };
29
+ if (port.direction === 'OUTPUT') {
30
+ outputs[port.name] = def;
31
+ }
32
+ else {
33
+ inputs[port.name] = def;
34
+ }
35
+ }
36
+ }
37
+ // Ensure mandatory ports exist
38
+ if (!inputs.execute) {
39
+ inputs.execute = { dataType: 'STEP', label: 'Execute' };
40
+ }
41
+ if (!outputs.onSuccess) {
42
+ outputs.onSuccess = { dataType: 'STEP', label: 'On Success', isControlFlow: true };
43
+ }
44
+ if (!outputs.onFailure) {
45
+ outputs.onFailure = {
46
+ dataType: 'STEP',
47
+ label: 'On Failure',
48
+ isControlFlow: true,
49
+ failure: true,
50
+ };
51
+ }
52
+ return {
53
+ type: 'NodeType',
54
+ name: ext.name,
55
+ functionName: ext.functionName || ext.name,
56
+ inputs,
57
+ outputs,
58
+ hasSuccessPort: 'onSuccess' in outputs,
59
+ hasFailurePort: 'onFailure' in outputs,
60
+ isAsync: false,
61
+ executeWhen: EXECUTION_STRATEGIES.CONJUNCTION,
62
+ variant: 'FUNCTION',
63
+ };
64
+ }
65
+ // Port ordering functions imported from ./utils/port-ordering
66
+ /** Exposed for tests that need direct access to the shared ts-morph Project */
67
+ export function getParserProject() {
68
+ return getSharedProject();
69
+ }
70
+ export class AnnotationParser {
71
+ project;
72
+ importCache = new LRUCache(200);
73
+ importStack = new Set();
74
+ parseCache = new LRUCache(100);
75
+ constructor() {
76
+ this.project = getSharedProject();
77
+ }
78
+ computeHash(content) {
79
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
80
+ }
81
+ detectMinorEdit(original, updated) {
82
+ let start = 0;
83
+ const minLen = Math.min(original.length, updated.length);
84
+ while (start < minLen && original[start] === updated[start])
85
+ start++;
86
+ let endOrig = original.length;
87
+ let endNew = updated.length;
88
+ while (endOrig > start && endNew > start && original[endOrig - 1] === updated[endNew - 1]) {
89
+ endOrig--;
90
+ endNew--;
91
+ }
92
+ const changedRegion = updated.slice(start, endNew);
93
+ // Structural patterns require full re-parse
94
+ const structural = /import\b|export\b|@flowWeaver|@input\b|@output\b|function\s+\w+\s*\(|const\s+\w+\s*=|let\s+\w+\s*=|var\s+\w+\s*=|@node\b|@connect\b/;
95
+ if (structural.test(changedRegion)) {
96
+ return { isMinor: false, affectedFunctions: [] };
97
+ }
98
+ // For now, return isMinor: true but no affected functions (conservative approach)
99
+ // This means we'll still do a full parse but the infrastructure is in place
100
+ return { isMinor: true, affectedFunctions: [] };
101
+ }
102
+ patchAST(filePath, cached, newContent, _affectedFunctions) {
103
+ try {
104
+ const sourceFile = this.project.getSourceFile(filePath);
105
+ if (!sourceFile)
106
+ return null;
107
+ sourceFile.replaceWithText(newContent);
108
+ // Re-extract all node types (conservative approach for now)
109
+ const warnings = [];
110
+ const nodeTypes = this.extractNodeTypes(sourceFile, warnings);
111
+ const result = {
112
+ ...cached.result,
113
+ nodeTypes,
114
+ warnings: [...cached.result.warnings, ...warnings],
115
+ };
116
+ this.parseCache.set(filePath, {
117
+ mtime: fs.statSync(filePath).mtimeMs,
118
+ contentHash: this.computeHash(newContent),
119
+ result,
120
+ });
121
+ return result;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ parse(filePath, externalNodeTypes) {
128
+ const stats = fs.statSync(filePath);
129
+ const hasExternalTypes = externalNodeTypes && externalNodeTypes.length > 0;
130
+ // Skip cache when external node types are provided — cache was built without them
131
+ if (!hasExternalTypes) {
132
+ const cached = this.parseCache.get(filePath);
133
+ // FAST PATH 1: mtime unchanged
134
+ if (cached && cached.mtime === stats.mtimeMs) {
135
+ return cached.result;
136
+ }
137
+ const rawContent = fs.readFileSync(filePath, 'utf-8');
138
+ const content = hasInPlaceMarkers(rawContent)
139
+ ? stripGeneratedSections(rawContent)
140
+ : rawContent;
141
+ const hash = this.computeHash(content);
142
+ // FAST PATH 2: content hash unchanged (save without edit)
143
+ if (cached && cached.contentHash === hash) {
144
+ cached.mtime = stats.mtimeMs;
145
+ return cached.result;
146
+ }
147
+ // FAST PATH 3: Incremental patching disabled — re-enable when detectMinorEdit
148
+ // returns affected functions. Infrastructure preserved in detectMinorEdit/patchAST.
149
+ // FALLBACK: Full parse
150
+ return this.fullParse(filePath, content, hash, stats.mtimeMs);
151
+ }
152
+ // External types provided — always do a full parse without caching the result
153
+ const rawContent = fs.readFileSync(filePath, 'utf-8');
154
+ const content = hasInPlaceMarkers(rawContent) ? stripGeneratedSections(rawContent) : rawContent;
155
+ const hash = this.computeHash(content);
156
+ return this.fullParse(filePath, content, hash, stats.mtimeMs, externalNodeTypes);
157
+ }
158
+ fullParse(filePath, content, hash, mtimeMs, externalNodeTypes) {
159
+ // Reset import tracking for new parse
160
+ this.importStack.clear();
161
+ const errors = [];
162
+ const warnings = [];
163
+ const sourceFile = this.project.createSourceFile(filePath, content, { overwrite: true });
164
+ // Add current file to import stack BEFORE processing imports
165
+ this.importStack.add(filePath);
166
+ const localNodeTypes = this.extractNodeTypes(sourceFile, warnings);
167
+ const importedNodeTypes = this.extractImportedNodeTypes(sourceFile, filePath);
168
+ // First pass: extract workflow signatures to enable same-file workflow invocation
169
+ const workflowSignatures = this.extractWorkflowSignatures(sourceFile, filePath, warnings);
170
+ const sameFileWorkflowNodeTypes = workflowSignatures.map((wf) => this.workflowToNodeType(wf));
171
+ const nodeTypes = [...localNodeTypes, ...importedNodeTypes, ...sameFileWorkflowNodeTypes];
172
+ // Merge external (runtime-loaded) node types so the parser can validate references
173
+ if (externalNodeTypes?.length) {
174
+ for (const ext of externalNodeTypes) {
175
+ const alreadyKnown = nodeTypes.some((nt) => nt.name === ext.name || nt.functionName === ext.name);
176
+ if (!alreadyKnown) {
177
+ nodeTypes.push(externalToAST(ext));
178
+ }
179
+ }
180
+ }
181
+ // Auto-infer node types from unannotated functions referenced by @node
182
+ const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
183
+ nodeTypes.push(...inferredNodeTypes);
184
+ const workflows = this.extractWorkflows(sourceFile, nodeTypes, filePath, errors, warnings);
185
+ const patterns = this.extractPatterns(sourceFile, nodeTypes, filePath, errors, warnings);
186
+ const result = { workflows, nodeTypes, patterns, errors, warnings };
187
+ // Clean up source file to prevent ts-morph Project bloat
188
+ // (results are captured in the returned AST, source file is no longer needed)
189
+ this.project.removeSourceFile(sourceFile);
190
+ // Only cache when no external types were used (cache should reflect file-only state)
191
+ if (!externalNodeTypes?.length) {
192
+ this.parseCache.set(filePath, {
193
+ mtime: mtimeMs,
194
+ contentHash: hash,
195
+ result,
196
+ });
197
+ }
198
+ return result;
199
+ }
200
+ /**
201
+ * Parse workflow from a string instead of a file path.
202
+ * Useful for testing and in-memory operations.
203
+ *
204
+ * Note: Imports from other workflow files are NOT supported in this mode
205
+ * since there's no filesystem context. Use parse() for files with imports.
206
+ *
207
+ * @param code - TypeScript source code containing workflow definitions
208
+ * @param virtualPath - Virtual file path for error messages (default: 'virtual.ts')
209
+ * @returns ParseResult with workflows and nodeTypes
210
+ */
211
+ parseFromString(code, virtualPath = 'virtual.ts') {
212
+ // Reset import tracking
213
+ this.importStack.clear();
214
+ // Remove existing virtual file if present
215
+ const existingFile = this.project.getSourceFile(virtualPath);
216
+ if (existingFile) {
217
+ this.project.removeSourceFile(existingFile);
218
+ }
219
+ // Create source file from string
220
+ const sourceFile = this.project.createSourceFile(virtualPath, code, { overwrite: true });
221
+ const errors = [];
222
+ const warnings = [];
223
+ const localNodeTypes = this.extractNodeTypes(sourceFile, warnings);
224
+ // First pass: extract workflow signatures to enable same-file workflow invocation
225
+ const workflowSignatures = this.extractWorkflowSignatures(sourceFile, virtualPath, warnings);
226
+ const sameFileWorkflowNodeTypes = workflowSignatures.map((wf) => this.workflowToNodeType(wf));
227
+ const nodeTypes = [...localNodeTypes, ...sameFileWorkflowNodeTypes];
228
+ // Auto-infer node types from unannotated functions referenced by @node
229
+ const inferredNodeTypes = this.inferNodeTypesFromUnannotated(sourceFile, nodeTypes);
230
+ nodeTypes.push(...inferredNodeTypes);
231
+ // Note: imports not supported for virtual files - would need filesystem access
232
+ const workflows = this.extractWorkflows(sourceFile, nodeTypes, virtualPath, errors, warnings);
233
+ const patterns = this.extractPatterns(sourceFile, nodeTypes, virtualPath, errors, warnings);
234
+ // Clean up virtual source file to prevent memory bloat
235
+ // (tests create many unique virtual paths that accumulate)
236
+ this.project.removeSourceFile(sourceFile);
237
+ return {
238
+ workflows,
239
+ nodeTypes,
240
+ patterns,
241
+ errors,
242
+ warnings,
243
+ };
244
+ }
245
+ clearCache() {
246
+ this.importCache.clear();
247
+ this.parseCache.clear();
248
+ }
249
+ /** Clear only the parse result cache, keeping the import/node-type cache intact. */
250
+ clearParseCache() {
251
+ this.parseCache.clear();
252
+ }
253
+ resolveModulePath(moduleSpecifier, currentDir) {
254
+ const extensions = ['.ts', '.tsx', '.js', '.jsx'];
255
+ // If already has extension, check if exists
256
+ const hasExtension = extensions.some((ext) => moduleSpecifier.endsWith(ext));
257
+ if (hasExtension) {
258
+ const fullPath = path.resolve(currentDir, moduleSpecifier);
259
+ return fs.existsSync(fullPath) ? fullPath : null;
260
+ }
261
+ // Try each extension in order
262
+ for (const ext of extensions) {
263
+ const fullPath = path.resolve(currentDir, moduleSpecifier + ext);
264
+ if (fs.existsSync(fullPath)) {
265
+ return fullPath;
266
+ }
267
+ }
268
+ // Try as directory with package.json main field or index file
269
+ const dirPath = path.resolve(currentDir, moduleSpecifier);
270
+ if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
271
+ // Check package.json main field
272
+ const pkgPath = path.join(dirPath, 'package.json');
273
+ if (fs.existsSync(pkgPath)) {
274
+ try {
275
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
276
+ if (pkg.main) {
277
+ const mainPath = path.resolve(dirPath, pkg.main);
278
+ if (fs.existsSync(mainPath)) {
279
+ return mainPath;
280
+ }
281
+ }
282
+ }
283
+ catch (e) {
284
+ console.warn(`Failed to parse package.json at ${pkgPath}: ${getErrorMessage(e)}`);
285
+ }
286
+ }
287
+ // Try index files
288
+ for (const ext of extensions) {
289
+ const indexPath = path.join(dirPath, `index${ext}`);
290
+ if (fs.existsSync(indexPath)) {
291
+ return indexPath;
292
+ }
293
+ }
294
+ }
295
+ return null;
296
+ }
297
+ extractImportedNodeTypes(sourceFile, currentFilePath) {
298
+ const importedNodeTypes = [];
299
+ const imports = sourceFile.getImportDeclarations();
300
+ for (const importDecl of imports) {
301
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
302
+ // Skip if module specifier is undefined or not a relative import
303
+ // Any .ts file with @flowWeaver annotations can be imported.
304
+ if (!moduleSpecifier) {
305
+ continue;
306
+ }
307
+ if (!moduleSpecifier.startsWith('.')) {
308
+ const packageNodeTypes = this.resolveNpmPackageTypes(importDecl, moduleSpecifier, currentFilePath);
309
+ importedNodeTypes.push(...packageNodeTypes);
310
+ continue;
311
+ }
312
+ const currentDir = path.dirname(currentFilePath);
313
+ const importedFilePath = this.resolveModulePath(moduleSpecifier, currentDir);
314
+ // Validate import path exists
315
+ if (!importedFilePath) {
316
+ throw new Error(`Import error: File not found for "${moduleSpecifier}"\n` +
317
+ ` Imported from: ${currentFilePath}\n` +
318
+ ` Searched extensions: .ts, .tsx, .js, .jsx`);
319
+ }
320
+ // Check for circular dependencies
321
+ if (this.importStack.has(importedFilePath)) {
322
+ const cycle = Array.from(this.importStack).concat(importedFilePath);
323
+ throw new Error(`Circular dependency detected:\n ${cycle.join('\n -> ')}`);
324
+ }
325
+ try {
326
+ // Check cache first
327
+ let nodeTypes;
328
+ if (this.importCache.has(importedFilePath)) {
329
+ nodeTypes = this.importCache.get(importedFilePath);
330
+ }
331
+ else {
332
+ // Add to import stack for circular dependency detection
333
+ this.importStack.add(importedFilePath);
334
+ try {
335
+ const importedRaw = fs.readFileSync(importedFilePath, 'utf-8');
336
+ const importedContent = hasInPlaceMarkers(importedRaw)
337
+ ? stripGeneratedSections(importedRaw)
338
+ : importedRaw;
339
+ const importedFile = this.project.createSourceFile(importedFilePath, importedContent, {
340
+ overwrite: true,
341
+ });
342
+ const importWarnings = [];
343
+ const localNodeTypes = this.extractNodeTypes(importedFile, importWarnings);
344
+ // Recursively process imports (enables circular dependency detection)
345
+ const importedFromFile = this.extractImportedNodeTypes(importedFile, importedFilePath);
346
+ // Also extract workflows and convert them to node types
347
+ const workflows = this.extractWorkflows(importedFile, [...localNodeTypes, ...importedFromFile], importedFilePath, [], importWarnings);
348
+ const workflowAsNodeTypes = workflows.map((wf) => this.workflowToNodeType(wf));
349
+ nodeTypes = [...localNodeTypes, ...importedFromFile, ...workflowAsNodeTypes];
350
+ // Pre-infer all unannotated functions so the named-import filter can resolve them
351
+ const inferredFromImport = this.inferAllUnannotatedFunctions(importedFile, nodeTypes);
352
+ nodeTypes.push(...inferredFromImport);
353
+ // Clean up imported source file to prevent Project bloat
354
+ this.project.removeSourceFile(importedFile);
355
+ // Cache the parsed node types
356
+ this.importCache.set(importedFilePath, nodeTypes);
357
+ }
358
+ finally {
359
+ // Remove from stack after processing
360
+ this.importStack.delete(importedFilePath);
361
+ }
362
+ }
363
+ // Extract only the named imports
364
+ const importedNames = new Set();
365
+ importDecl.getNamedImports().forEach((namedImport) => {
366
+ importedNames.add(namedImport.getName());
367
+ });
368
+ // Only include imports that are actually node types
369
+ // (other imports may be regular TypeScript exports like types, constants, etc.)
370
+ nodeTypes.forEach((nodeType) => {
371
+ if (importedNames.has(nodeType.functionName)) {
372
+ importedNodeTypes.push({
373
+ ...nodeType,
374
+ sourceLocation: {
375
+ file: importedFilePath,
376
+ line: nodeType.sourceLocation?.line || 0,
377
+ column: nodeType.sourceLocation?.column || 0,
378
+ },
379
+ });
380
+ }
381
+ });
382
+ }
383
+ catch (error) {
384
+ // Re-throw with better context
385
+ if (error instanceof Error) {
386
+ throw new Error(`Failed to process import from ${importedFilePath}:\n ${error.message}`);
387
+ }
388
+ throw error;
389
+ }
390
+ }
391
+ return importedNodeTypes;
392
+ }
393
+ /**
394
+ * Resolve npm package imports to node types by reading `.d.ts` declarations.
395
+ * Only named imports of exported functions are resolved.
396
+ */
397
+ resolveNpmPackageTypes(importDecl, moduleSpecifier, currentFilePath) {
398
+ // Only handle named imports
399
+ const namedImports = importDecl.getNamedImports();
400
+ if (namedImports.length === 0)
401
+ return [];
402
+ const importedNames = new Set();
403
+ namedImports.forEach((ni) => importedNames.add(ni.getName()));
404
+ // Check cache
405
+ const cacheKey = `npm:${moduleSpecifier}`;
406
+ if (this.importCache.has(cacheKey)) {
407
+ return this.importCache.get(cacheKey).filter((nt) => importedNames.has(nt.functionName));
408
+ }
409
+ // Resolve .d.ts path
410
+ const currentDir = path.dirname(currentFilePath);
411
+ const dtsPath = resolvePackageTypesPath(moduleSpecifier, currentDir);
412
+ if (!dtsPath)
413
+ return [];
414
+ try {
415
+ const dtsContent = fs.readFileSync(dtsPath, 'utf-8');
416
+ const dtsFile = this.project.createSourceFile(`__npm_dts__/${moduleSpecifier}.d.ts`, dtsContent, { overwrite: true });
417
+ const fns = extractFunctionLikes(dtsFile);
418
+ const allNodeTypes = [];
419
+ const seenNames = new Set();
420
+ for (const fn of fns) {
421
+ const fnName = fn.getName();
422
+ if (!fnName)
423
+ continue;
424
+ // Skip duplicate function names (overloaded declarations in .d.ts)
425
+ if (seenNames.has(fnName))
426
+ continue;
427
+ seenNames.add(fnName);
428
+ const nodeType = this.inferNodeTypeFromFunction(fn, fnName, dtsPath);
429
+ // Mark as npm package import and prevent inlining
430
+ nodeType.importSource = moduleSpecifier;
431
+ nodeType.functionText = undefined;
432
+ allNodeTypes.push(nodeType);
433
+ }
434
+ // Clean up the temporary source file
435
+ this.project.removeSourceFile(dtsFile);
436
+ // Cache all node types from this package
437
+ this.importCache.set(cacheKey, allNodeTypes);
438
+ // Return only the ones in the import statement
439
+ return allNodeTypes.filter((nt) => importedNames.has(nt.functionName));
440
+ }
441
+ catch {
442
+ // Silently skip packages whose .d.ts can't be parsed
443
+ return [];
444
+ }
445
+ }
446
+ /**
447
+ * Resolve an @fwImport annotation to a properly inferred node type.
448
+ * Supports both npm packages (e.g., "lodash") and relative paths (e.g., "./utils").
449
+ *
450
+ * @param imp - The import annotation from JSDoc
451
+ * @param currentFilePath - Path of the workflow file containing the @fwImport
452
+ * @param warnings - Array to collect warnings
453
+ * @returns Inferred TNodeTypeAST, or a stub if inference fails
454
+ */
455
+ resolveImportAnnotation(imp, currentFilePath, warnings) {
456
+ const currentDir = path.dirname(currentFilePath);
457
+ // Determine if this is a relative path import or an npm package
458
+ if (imp.importSource.startsWith('.')) {
459
+ // Relative path import - resolve local file and infer
460
+ return this.resolveLocalImportAnnotation(imp, currentDir, warnings);
461
+ }
462
+ else {
463
+ // npm package import - use .d.ts inference
464
+ return this.resolveNpmImportAnnotation(imp, currentDir);
465
+ }
466
+ }
467
+ /**
468
+ * Resolve a relative path @fwImport to a node type by reading the local file.
469
+ * Includes circular dependency detection using importStack.
470
+ */
471
+ resolveLocalImportAnnotation(imp, currentDir, warnings) {
472
+ const importedFilePath = this.resolveModulePath(imp.importSource, currentDir);
473
+ if (!importedFilePath) {
474
+ // Gap 3: Warn when relative path doesn't resolve
475
+ warnings.push(`@fwImport: Could not resolve "${imp.importSource}" from ${currentDir}`);
476
+ return this.createImportStub(imp);
477
+ }
478
+ // Gap 1: Circular dependency detection
479
+ if (this.importStack.has(importedFilePath)) {
480
+ const cycle = Array.from(this.importStack).concat(importedFilePath);
481
+ warnings.push(`@fwImport: Circular dependency detected:\n ${cycle.join('\n -> ')}`);
482
+ return this.createImportStub(imp);
483
+ }
484
+ // Add to import stack before processing
485
+ this.importStack.add(importedFilePath);
486
+ try {
487
+ const importedContent = fs.readFileSync(importedFilePath, 'utf-8');
488
+ const importedFile = this.project.createSourceFile(importedFilePath, importedContent, {
489
+ overwrite: true,
490
+ });
491
+ const fns = extractFunctionLikes(importedFile);
492
+ const fn = fns.find((f) => f.getName() === imp.functionName);
493
+ if (!fn) {
494
+ // Function not found in file - return stub
495
+ this.project.removeSourceFile(importedFile);
496
+ return this.createImportStub(imp);
497
+ }
498
+ // Infer BEFORE removing the source file (ts-morph needs it)
499
+ const nodeType = this.inferNodeTypeFromFunction(fn, imp.name, importedFilePath);
500
+ nodeType.importSource = imp.importSource;
501
+ nodeType.functionText = undefined; // Don't inline external code
502
+ // Clean up after inference is complete
503
+ this.project.removeSourceFile(importedFile);
504
+ return nodeType;
505
+ }
506
+ catch {
507
+ // Graceful fallback on any error
508
+ return this.createImportStub(imp);
509
+ }
510
+ finally {
511
+ // Always remove from import stack
512
+ this.importStack.delete(importedFilePath);
513
+ }
514
+ }
515
+ /**
516
+ * Resolve an npm package @fwImport to a node type by reading .d.ts declarations.
517
+ */
518
+ resolveNpmImportAnnotation(imp, currentDir) {
519
+ // Check cache
520
+ const cacheKey = `npm:${imp.importSource}`;
521
+ if (this.importCache.has(cacheKey)) {
522
+ const cached = this.importCache.get(cacheKey);
523
+ const found = cached.find((nt) => nt.functionName === imp.functionName);
524
+ if (found) {
525
+ // Return a copy with the correct name from @fwImport
526
+ return { ...found, name: imp.name, importSource: imp.importSource };
527
+ }
528
+ }
529
+ // Resolve .d.ts path
530
+ const dtsPath = resolvePackageTypesPath(imp.importSource, currentDir);
531
+ if (!dtsPath) {
532
+ return this.createImportStub(imp);
533
+ }
534
+ try {
535
+ const dtsContent = fs.readFileSync(dtsPath, 'utf-8');
536
+ const dtsFile = this.project.createSourceFile(`__npm_dts__/${imp.importSource}.d.ts`, dtsContent, { overwrite: true });
537
+ const fns = extractFunctionLikes(dtsFile);
538
+ const allNodeTypes = [];
539
+ const seenNames = new Set();
540
+ for (const fn of fns) {
541
+ const fnName = fn.getName();
542
+ if (!fnName)
543
+ continue;
544
+ // Skip duplicate function names (overloaded declarations in .d.ts)
545
+ if (seenNames.has(fnName))
546
+ continue;
547
+ seenNames.add(fnName);
548
+ const nodeType = this.inferNodeTypeFromFunction(fn, fnName, dtsPath);
549
+ nodeType.importSource = imp.importSource;
550
+ nodeType.functionText = undefined;
551
+ allNodeTypes.push(nodeType);
552
+ }
553
+ this.project.removeSourceFile(dtsFile);
554
+ // Cache all node types from this package
555
+ this.importCache.set(cacheKey, allNodeTypes);
556
+ // Find the specific function we need
557
+ const found = allNodeTypes.find((nt) => nt.functionName === imp.functionName);
558
+ if (found) {
559
+ return { ...found, name: imp.name, importSource: imp.importSource };
560
+ }
561
+ }
562
+ catch {
563
+ // Silently skip packages whose .d.ts can't be parsed
564
+ }
565
+ return this.createImportStub(imp);
566
+ }
567
+ /**
568
+ * Create a stub node type for @fwImport when proper inference fails.
569
+ * This provides graceful degradation rather than failing completely.
570
+ */
571
+ createImportStub(imp) {
572
+ return {
573
+ type: 'NodeType',
574
+ name: imp.name,
575
+ functionName: imp.functionName,
576
+ importSource: imp.importSource,
577
+ variant: 'FUNCTION',
578
+ inputs: {},
579
+ outputs: { result: { dataType: 'ANY' } },
580
+ hasSuccessPort: true,
581
+ hasFailurePort: true,
582
+ executeWhen: 'CONJUNCTION',
583
+ isAsync: false,
584
+ // Mark as expression since most npm functions are pure
585
+ // This is a reasonable default for stubs
586
+ expression: true,
587
+ };
588
+ }
589
+ extractNodeTypes(sourceFile, warnings) {
590
+ const nodeTypes = [];
591
+ extractFunctionLikes(sourceFile).forEach((fn) => {
592
+ // Parse JSDoc comments
593
+ const config = jsdocParser.parseNodeType(fn, warnings);
594
+ if (!config) {
595
+ const jsdocText = fn.getJsDocs().map((d) => d.getFullText()).join('');
596
+ if (jsdocText.includes('@flowWeaver') && jsdocText.includes('nodeType')) {
597
+ warnings.push(`Function "${fn.getName() || 'anonymous'}" has @flowWeaver annotation but could not be parsed. ` +
598
+ `Check for special characters (---) or malformed JSDoc syntax.`);
599
+ }
600
+ return;
601
+ }
602
+ const functionName = fn.getName() || 'anonymous';
603
+ const nodeTypeName = config.name || functionName;
604
+ const inputs = {};
605
+ if (config.inputs) {
606
+ for (const [portName, portDef] of Object.entries(config.inputs)) {
607
+ inputs[portName] = {
608
+ dataType: portDef.type,
609
+ default: portDef.defaultValue,
610
+ optional: portDef.optional,
611
+ label: portDef.label,
612
+ expression: portDef.expression,
613
+ ...(portDef.scope && { scope: portDef.scope }),
614
+ ...(portDef.metadata && { metadata: portDef.metadata }),
615
+ ...(portDef.tsType && { tsType: portDef.tsType }),
616
+ };
617
+ }
618
+ }
619
+ const outputs = {};
620
+ if (config.outputs) {
621
+ for (const [portName, portDef] of Object.entries(config.outputs)) {
622
+ outputs[portName] = {
623
+ dataType: portDef.type,
624
+ label: portDef.label,
625
+ ...(portDef.scope && { scope: portDef.scope }),
626
+ ...(portDef.metadata && { metadata: portDef.metadata }),
627
+ ...(portDef.tsType && { tsType: portDef.tsType }),
628
+ };
629
+ }
630
+ }
631
+ // Auto-infer ports for @expression nodes when @input/@output are missing.
632
+ // If the function has @expression but no explicit port annotations, infer
633
+ // data ports from the TypeScript function signature (same logic as unannotated functions).
634
+ if (config.expression) {
635
+ const hasExplicitDataInputs = Object.keys(inputs).some((k) => k !== 'execute');
636
+ const hasExplicitDataOutputs = Object.keys(outputs).some((k) => k !== 'onSuccess' && k !== 'onFailure');
637
+ if (!hasExplicitDataInputs || !hasExplicitDataOutputs) {
638
+ const inferred = this.inferNodeTypeFromFunction(fn, nodeTypeName, fn.getSourceFile().getFilePath());
639
+ if (!hasExplicitDataInputs) {
640
+ // Copy inferred data inputs (skip control flow ports)
641
+ for (const [portName, portDef] of Object.entries(inferred.inputs)) {
642
+ if (portName === 'execute')
643
+ continue;
644
+ inputs[portName] = portDef;
645
+ }
646
+ }
647
+ if (!hasExplicitDataOutputs) {
648
+ // Copy inferred data outputs (skip control flow ports)
649
+ for (const [portName, portDef] of Object.entries(inferred.outputs)) {
650
+ if (portName === 'onSuccess' || portName === 'onFailure')
651
+ continue;
652
+ outputs[portName] = portDef;
653
+ }
654
+ }
655
+ }
656
+ }
657
+ // ALL nodes must have execute input and onSuccess/onFailure outputs
658
+ // Execute port is visible by default so users can connect execution flow
659
+ // Merge user-defined ports with mandatory defaults to preserve special properties
660
+ inputs.execute = {
661
+ label: 'Execute', // Default label
662
+ ...inputs.execute, // User can override label
663
+ dataType: 'STEP', // But dataType is mandatory
664
+ };
665
+ outputs.onSuccess = {
666
+ label: 'On Success', // Default label
667
+ ...outputs.onSuccess, // User can override label
668
+ dataType: 'STEP', // But dataType is mandatory
669
+ isControlFlow: true, // Always a control flow port
670
+ };
671
+ outputs.onFailure = {
672
+ label: 'On Failure', // Default label
673
+ ...outputs.onFailure, // User can override label
674
+ dataType: 'STEP', // But dataType is mandatory
675
+ failure: true, // Always a failure port
676
+ isControlFlow: true, // Always a control flow port
677
+ };
678
+ // Assign implicit port orders with mandatory port precedence
679
+ assignImplicitPortOrders(inputs);
680
+ assignImplicitPortOrders(outputs);
681
+ // Get function text (JSDoc comment + function)
682
+ // getText() returns just the function, so we need to prepend the JSDoc
683
+ const jsDocs = fn.getJsDocs();
684
+ const jsDocText = jsDocs.map((doc) => doc.getText()).join('\n');
685
+ const functionText = jsDocText ? `${jsDocText}\n${fn.getText()}` : fn.getText();
686
+ // Detect async keyword on function declaration
687
+ const isAsync = fn.isAsync();
688
+ // Convert defaultConfig
689
+ let defaultConfig = undefined;
690
+ if (config.defaultConfig) {
691
+ defaultConfig = {
692
+ label: config.defaultConfig.label,
693
+ description: config.defaultConfig.description,
694
+ pullExecution: config.defaultConfig.pullExecution,
695
+ };
696
+ }
697
+ // Extract unique scope names from ports (per-port scoped architecture)
698
+ const portScopes = new Set();
699
+ Object.values(inputs).forEach((port) => {
700
+ if (port.scope)
701
+ portScopes.add(port.scope);
702
+ });
703
+ Object.values(outputs).forEach((port) => {
704
+ if (port.scope)
705
+ portScopes.add(port.scope);
706
+ });
707
+ // Determine scopes array:
708
+ // - If node has node-level scope: use that (old architecture)
709
+ // - Otherwise if ports have scopes: use unique port scopes (per-port architecture)
710
+ // - Otherwise: undefined (no scopes)
711
+ const scopes = config.scope
712
+ ? [config.scope]
713
+ : portScopes.size > 0
714
+ ? Array.from(portScopes)
715
+ : undefined;
716
+ nodeTypes.push({
717
+ type: 'NodeType',
718
+ name: nodeTypeName,
719
+ functionName,
720
+ variant: 'FUNCTION',
721
+ inputs,
722
+ outputs,
723
+ hasSuccessPort: RESERVED_PORT_NAMES.ON_SUCCESS in outputs,
724
+ hasFailurePort: RESERVED_PORT_NAMES.ON_FAILURE in outputs,
725
+ isAsync,
726
+ functionText,
727
+ executeWhen: config.executeWhen || EXECUTION_STRATEGIES.CONJUNCTION,
728
+ defaultConfig,
729
+ scope: config.scope,
730
+ scopes,
731
+ ...(config.expression && { expression: true }),
732
+ ...(fn.getDeclarationKind?.() && { declarationKind: fn.getDeclarationKind() }),
733
+ label: config.label,
734
+ description: config.description,
735
+ visuals: config.color || config.icon || config.tags
736
+ ? {
737
+ color: config.color,
738
+ icon: config.icon,
739
+ tags: config.tags,
740
+ }
741
+ : undefined,
742
+ sourceLocation: {
743
+ file: sourceFile.getFilePath(),
744
+ line: fn.getStartLineNumber(false),
745
+ column: 0,
746
+ },
747
+ });
748
+ });
749
+ return nodeTypes;
750
+ }
751
+ /**
752
+ * Extract workflow signatures (metadata only) without validating instances.
753
+ * This enables same-file workflow invocation by making workflow ports available
754
+ * before the full workflow extraction pass.
755
+ */
756
+ extractWorkflowSignatures(sourceFile, filePath, warnings) {
757
+ const workflows = [];
758
+ extractFunctionLikes(sourceFile).forEach((fn) => {
759
+ const config = jsdocParser.parseWorkflow(fn, warnings);
760
+ if (!config)
761
+ return;
762
+ const functionName = fn.getName() || 'anonymous';
763
+ const startPorts = this.parseStartPorts(fn, config);
764
+ const exitPorts = this.parseExitPorts(fn, config);
765
+ const userSpecifiedAsync = fn.isAsync();
766
+ workflows.push({
767
+ type: 'Workflow',
768
+ sourceFile: filePath,
769
+ name: config.name || functionName,
770
+ functionName,
771
+ nodeTypes: [],
772
+ instances: [],
773
+ connections: [],
774
+ startPorts,
775
+ exitPorts,
776
+ imports: [],
777
+ description: config.description,
778
+ userSpecifiedAsync,
779
+ });
780
+ });
781
+ return workflows;
782
+ }
783
+ /**
784
+ * Convert a workflow to a node type.
785
+ * This allows workflows to be used as nodes in other workflows.
786
+ */
787
+ workflowToNodeType(workflow) {
788
+ return {
789
+ type: 'NodeType',
790
+ name: workflow.name,
791
+ functionName: workflow.functionName,
792
+ variant: 'IMPORTED_WORKFLOW',
793
+ path: workflow.sourceFile,
794
+ inputs: { ...workflow.startPorts },
795
+ outputs: { ...workflow.exitPorts },
796
+ hasSuccessPort: 'onSuccess' in workflow.exitPorts,
797
+ hasFailurePort: 'onFailure' in workflow.exitPorts,
798
+ isAsync: workflow.userSpecifiedAsync || false,
799
+ executeWhen: EXECUTION_STRATEGIES.CONJUNCTION,
800
+ description: workflow.description,
801
+ sourceLocation: {
802
+ file: workflow.sourceFile,
803
+ line: 0,
804
+ column: 0,
805
+ },
806
+ };
807
+ }
808
+ extractWorkflows(sourceFile, availableNodeTypes, filePath, errors, warnings) {
809
+ const workflows = [];
810
+ const allFunctions = extractFunctionLikes(sourceFile);
811
+ // Collect all function names in the file for unannotated-function hints in validator
812
+ const allFunctionNames = allFunctions
813
+ .map((fn) => fn.getName())
814
+ .filter((name) => !!name);
815
+ allFunctions.forEach((fn) => {
816
+ // Parse JSDoc comments
817
+ const config = jsdocParser.parseWorkflow(fn, warnings);
818
+ if (!config) {
819
+ const jsdocText = fn.getJsDocs().map((d) => d.getFullText()).join('');
820
+ if (jsdocText.includes('@flowWeaver') && jsdocText.includes('workflow')) {
821
+ warnings.push(`Function "${fn.getName() || 'anonymous'}" has @flowWeaver annotation but could not be parsed. ` +
822
+ `Check for special characters (---) or malformed JSDoc syntax.`);
823
+ }
824
+ return;
825
+ }
826
+ const functionName = fn.getName() || 'anonymous';
827
+ // Validate no IN/OUT pseudo-nodes in workflows (they're only for patterns)
828
+ if (config.connections) {
829
+ for (const conn of config.connections) {
830
+ if (conn.from.node === 'IN' || conn.from.node === 'OUT') {
831
+ errors.push(`Workflow "${functionName}" uses "${conn.from.node}" pseudo-node which is only valid in patterns. Use "Start" or "Exit" instead.`);
832
+ }
833
+ if (conn.to.node === 'IN' || conn.to.node === 'OUT') {
834
+ errors.push(`Workflow "${functionName}" uses "${conn.to.node}" pseudo-node which is only valid in patterns. Use "Start" or "Exit" instead.`);
835
+ }
836
+ }
837
+ }
838
+ // Detect async keyword on workflow function declaration
839
+ const userSpecifiedAsync = fn.isAsync();
840
+ const startPorts = this.parseStartPorts(fn, config);
841
+ const exitPorts = this.parseExitPorts(fn, config);
842
+ // Convert @fwImport annotations to properly inferred node types
843
+ // These are persisted in JSDoc so they survive file re-parsing
844
+ // Uses the same inference logic as TS imports for consistency
845
+ const importedNpmNodeTypes = (config.imports || []).map((imp) => this.resolveImportAnnotation(imp, filePath, warnings));
846
+ // Combine available node types with imported npm types for validation
847
+ const allAvailableNodeTypes = [...availableNodeTypes, ...importedNpmNodeTypes];
848
+ // Convert instances to NodeInstanceAST
849
+ const instances = (config.instances || []).map((inst) => {
850
+ // Validate node type exists — push error instead of throwing so that
851
+ // partial parse results remain usable (defense-in-depth for race conditions)
852
+ const nodeTypeExists = allAvailableNodeTypes.some((nt) => nt.name === inst.type || nt.functionName === inst.type);
853
+ if (!nodeTypeExists) {
854
+ errors.push(`Node type "${inst.type}" not found in workflow "${functionName}". ` +
855
+ `Available types: ${allAvailableNodeTypes.map((nt) => nt.functionName).join(', ') || '(none)'}`);
856
+ }
857
+ const position = config.positions?.[inst.id];
858
+ // Convert parentScope string "nodeName.scope" to parent object
859
+ let parent;
860
+ if (inst.parentScope) {
861
+ const dotIndex = inst.parentScope.indexOf('.');
862
+ if (dotIndex > 0) {
863
+ parent = {
864
+ id: inst.parentScope.substring(0, dotIndex),
865
+ scope: inst.parentScope.substring(dotIndex + 1),
866
+ };
867
+ }
868
+ }
869
+ // portConfigs are direction-agnostic (annotations don't carry direction info).
870
+ // Matching code handles undefined direction by matching any direction.
871
+ const portConfigs = inst.portConfigs;
872
+ return {
873
+ type: 'NodeInstance',
874
+ id: inst.id,
875
+ nodeType: inst.type,
876
+ ...(parent && { parent }),
877
+ config: {
878
+ ...(inst.label && { label: inst.label }),
879
+ ...(position && { x: position.x, y: position.y }),
880
+ ...(portConfigs && portConfigs.length > 0 && { portConfigs }),
881
+ ...(inst.pullExecution && { pullExecution: inst.pullExecution }),
882
+ ...(inst.minimized && { minimized: inst.minimized }),
883
+ ...(inst.color && { color: inst.color }),
884
+ ...(inst.icon && { icon: inst.icon }),
885
+ ...(inst.tags && inst.tags.length > 0 && { tags: inst.tags }),
886
+ ...(inst.width && { width: inst.width }),
887
+ ...(inst.height && { height: inst.height }),
888
+ },
889
+ ...(inst.sourceLocation && {
890
+ sourceLocation: { file: filePath, ...inst.sourceLocation },
891
+ }),
892
+ };
893
+ });
894
+ // Convert connections to ConnectionAST
895
+ const connections = (config.connections || []).map((conn) => ({
896
+ type: 'Connection',
897
+ from: conn.from,
898
+ to: conn.to,
899
+ ...(conn.sourceLocation && { sourceLocation: { file: filePath, ...conn.sourceLocation } }),
900
+ }));
901
+ // Auto-connect: when @autoConnect is set and no explicit @connect annotations exist,
902
+ // auto-wire linear connections between nodes in declaration order
903
+ if (config.autoConnect && connections.length === 0 && instances.length > 0) {
904
+ const autoConnections = this.generateAutoConnections(instances, allAvailableNodeTypes, startPorts, exitPorts);
905
+ connections.push(...autoConnections);
906
+ }
907
+ // Expand @map macros into synthetic node types, instances, connections, and scopes
908
+ const scopes = config.scopes || {};
909
+ const macros = [];
910
+ if (config.maps && config.maps.length > 0) {
911
+ for (const mapConfig of config.maps) {
912
+ this.expandMapMacro(mapConfig, instances, connections, scopes, allAvailableNodeTypes, macros, errors, warnings);
913
+ }
914
+ }
915
+ // Expand @path macros into multi-step execution routes with scope walking
916
+ if (config.paths && config.paths.length > 0) {
917
+ this.expandPathMacros(config.paths, instances, connections, allAvailableNodeTypes, startPorts, exitPorts, macros, errors, warnings);
918
+ }
919
+ // Include ALL available nodeTypes in the workflow AST, plus imported npm types.
920
+ // Previously this filtered to only nodeTypes used by instances, but that caused
921
+ // a bug: when creating a new nodeType and then adding its first instance,
922
+ // the second operation would re-parse the file, see no instances using the
923
+ // nodeType yet, filter it out, and then removeOrphanedNodeTypeFunctions would
924
+ // delete the nodeType function that was just written.
925
+ // NPM types come from @import annotations in JSDoc (persisted to survive re-parsing).
926
+ // Deduplicate: @fwImport types take precedence over external/runtime types with the same name.
927
+ // Without this, each parse+generate cycle adds one more duplicate @fwImport entry.
928
+ const importedNames = new Set(importedNpmNodeTypes.map((nt) => nt.name));
929
+ // Use allAvailableNodeTypes (includes synthetic MAP_ITERATOR types from @map macros)
930
+ const dedupedAvailableTypes = allAvailableNodeTypes.filter((nt) => !importedNames.has(nt.name));
931
+ const workflowNodeTypes = [...dedupedAvailableTypes, ...importedNpmNodeTypes];
932
+ // Extract Start/Exit positions from config.positions
933
+ const ui = {};
934
+ const startPosition = config.positions?.['Start'];
935
+ if (startPosition) {
936
+ ui.startNode = { x: startPosition.x, y: startPosition.y };
937
+ }
938
+ const exitPosition = config.positions?.['Exit'];
939
+ if (exitPosition) {
940
+ ui.exitNode = { x: exitPosition.x, y: exitPosition.y };
941
+ }
942
+ workflows.push({
943
+ type: 'Workflow',
944
+ sourceFile: filePath,
945
+ name: config.name || functionName,
946
+ functionName: functionName,
947
+ nodeTypes: workflowNodeTypes,
948
+ instances,
949
+ connections,
950
+ scopes,
951
+ startPorts,
952
+ exitPorts,
953
+ imports: [],
954
+ description: config.description,
955
+ userSpecifiedAsync,
956
+ availableFunctionNames: allFunctionNames,
957
+ ...(macros.length > 0 && { macros }),
958
+ ...(Object.keys(ui).length > 0 && { ui }),
959
+ ...((config.strictTypes !== undefined || config.autoConnect ||
960
+ config.trigger || config.cancelOn || config.retries !== undefined ||
961
+ config.timeout || config.throttle) && {
962
+ options: {
963
+ ...(config.strictTypes !== undefined && { strictTypes: config.strictTypes }),
964
+ ...(config.autoConnect && { autoConnect: true }),
965
+ ...(config.trigger && { trigger: config.trigger }),
966
+ ...(config.cancelOn && { cancelOn: config.cancelOn }),
967
+ ...(config.retries !== undefined && { retries: config.retries }),
968
+ ...(config.timeout && { timeout: config.timeout }),
969
+ ...(config.throttle && { throttle: config.throttle }),
970
+ },
971
+ }),
972
+ });
973
+ });
974
+ return workflows;
975
+ }
976
+ /**
977
+ * Extract patterns from a source file.
978
+ * Patterns are defined with @flowWeaver pattern annotation.
979
+ */
980
+ extractPatterns(sourceFile, availableNodeTypes, filePath, errors, warnings) {
981
+ const patterns = [];
982
+ const seenNames = new Set();
983
+ extractFunctionLikes(sourceFile).forEach((fn) => {
984
+ // Parse JSDoc comments for pattern
985
+ const config = jsdocParser.parsePattern(fn, warnings);
986
+ if (!config) {
987
+ const jsdocText = fn.getJsDocs().map((d) => d.getFullText()).join('');
988
+ if (jsdocText.includes('@flowWeaver') && jsdocText.includes('pattern')) {
989
+ warnings.push(`Function "${fn.getName() || 'anonymous'}" has @flowWeaver annotation but could not be parsed. ` +
990
+ `Check for special characters (---) or malformed JSDoc syntax.`);
991
+ }
992
+ return;
993
+ }
994
+ // Validate required @name
995
+ if (!config.name) {
996
+ errors.push(`Pattern is missing required @name tag in function "${fn.getName()}"`);
997
+ return;
998
+ }
999
+ // Check for duplicate names
1000
+ if (seenNames.has(config.name)) {
1001
+ errors.push(`Duplicate pattern name "${config.name}" in file`);
1002
+ return;
1003
+ }
1004
+ seenNames.add(config.name);
1005
+ // Extract node types used by this pattern
1006
+ const patternNodeTypes = availableNodeTypes.filter((nt) => config.instances?.some((inst) => inst.nodeType === nt.name));
1007
+ // Build connections from config
1008
+ const connections = (config.connections || []).map((conn) => ({
1009
+ type: 'Connection',
1010
+ from: conn.from,
1011
+ to: conn.to,
1012
+ }));
1013
+ // Extract input/output ports from @port declarations
1014
+ const inputPorts = {};
1015
+ const outputPorts = {};
1016
+ if (config.ports) {
1017
+ for (const port of config.ports) {
1018
+ if (port.direction === 'IN') {
1019
+ inputPorts[port.name] = { description: port.description };
1020
+ }
1021
+ else if (port.direction === 'OUT') {
1022
+ outputPorts[port.name] = { description: port.description };
1023
+ }
1024
+ }
1025
+ }
1026
+ // Build instances from config
1027
+ const instances = (config.instances || []).map((inst) => ({
1028
+ type: 'NodeInstance',
1029
+ id: inst.id,
1030
+ nodeType: inst.nodeType,
1031
+ config: inst.config || {},
1032
+ }));
1033
+ patterns.push({
1034
+ type: 'Pattern',
1035
+ sourceFile: filePath,
1036
+ name: config.name,
1037
+ description: config.description,
1038
+ nodeTypes: patternNodeTypes,
1039
+ instances,
1040
+ connections,
1041
+ inputPorts,
1042
+ outputPorts,
1043
+ });
1044
+ });
1045
+ return patterns;
1046
+ }
1047
+ /**
1048
+ * Infer a TNodeTypeAST from a single function's TypeScript signature.
1049
+ * Shared helper used by both same-file and cross-file inference.
1050
+ */
1051
+ inferNodeTypeFromFunction(fn, name, filePath) {
1052
+ // Infer inputs from parameters
1053
+ const inputs = {};
1054
+ const params = fn.getParameters();
1055
+ const firstParamIsExecute = params.length > 0 && params[0].getName() === 'execute';
1056
+ for (const param of params) {
1057
+ const paramName = param.getName();
1058
+ const tsType = param.getType().getText(param);
1059
+ const dataType = inferDataTypeFromTS(tsType);
1060
+ const optional = param.isOptional() || param.hasInitializer();
1061
+ inputs[paramName] = {
1062
+ dataType,
1063
+ optional: optional || undefined,
1064
+ label: this.capitalize(paramName),
1065
+ tsType,
1066
+ };
1067
+ }
1068
+ // Infer outputs from return type
1069
+ const outputs = {};
1070
+ let returnType = fn.getReturnType();
1071
+ const returnTypeText = returnType.getText();
1072
+ // Unwrap Promise<T>
1073
+ if (returnTypeText.startsWith('Promise<')) {
1074
+ const typeArgs = returnType.getTypeArguments();
1075
+ if (typeArgs && typeArgs.length > 0) {
1076
+ returnType = typeArgs[0];
1077
+ }
1078
+ }
1079
+ const unwrappedText = returnType.getText();
1080
+ if (unwrappedText !== 'void' && unwrappedText !== 'undefined') {
1081
+ const primitiveTypes = new Set(['string', 'number', 'boolean', 'any', 'unknown', 'never']);
1082
+ const isPrimitive = primitiveTypes.has(unwrappedText);
1083
+ const isArray = unwrappedText.endsWith('[]') || unwrappedText.startsWith('Array<');
1084
+ const properties = returnType.getProperties();
1085
+ const isObjectLike = !isPrimitive && !isArray && returnType.isObject() && properties.length > 0;
1086
+ if (isObjectLike) {
1087
+ for (const prop of properties) {
1088
+ const propName = prop.getName();
1089
+ if (propName === 'onSuccess' || propName === 'onFailure')
1090
+ continue;
1091
+ const propType = prop.getTypeAtLocation(fn.getTypeResolutionNode());
1092
+ const propTypeText = propType.getText();
1093
+ const dataType = inferDataTypeFromTS(propTypeText);
1094
+ outputs[propName] = {
1095
+ dataType,
1096
+ label: this.capitalize(propName),
1097
+ tsType: propTypeText,
1098
+ };
1099
+ }
1100
+ }
1101
+ else {
1102
+ const dataType = inferDataTypeFromTS(unwrappedText);
1103
+ outputs.result = {
1104
+ dataType,
1105
+ label: 'Result',
1106
+ tsType: unwrappedText,
1107
+ };
1108
+ }
1109
+ }
1110
+ // Add mandatory ports
1111
+ inputs.execute = { dataType: 'STEP', label: 'Execute' };
1112
+ outputs.onSuccess = { dataType: 'STEP', label: 'On Success', isControlFlow: true };
1113
+ outputs.onFailure = {
1114
+ dataType: 'STEP',
1115
+ label: 'On Failure',
1116
+ failure: true,
1117
+ isControlFlow: true,
1118
+ };
1119
+ // Assign implicit port orders
1120
+ assignImplicitPortOrders(inputs);
1121
+ assignImplicitPortOrders(outputs);
1122
+ // Build TNodeTypeAST
1123
+ const jsDocs = fn.getJsDocs();
1124
+ const jsDocText = jsDocs.map((doc) => doc.getText()).join('\n');
1125
+ const functionText = jsDocText ? `${jsDocText}\n${fn.getText()}` : fn.getText();
1126
+ return {
1127
+ type: 'NodeType',
1128
+ name,
1129
+ functionName: name,
1130
+ variant: 'FUNCTION',
1131
+ inputs,
1132
+ outputs,
1133
+ hasSuccessPort: true,
1134
+ hasFailurePort: true,
1135
+ isAsync: fn.isAsync() || returnTypeText.startsWith('Promise<'),
1136
+ executeWhen: EXECUTION_STRATEGIES.CONJUNCTION,
1137
+ expression: !firstParamIsExecute, // Expression only if original function lacks execute as first param
1138
+ inferred: true,
1139
+ functionText,
1140
+ ...(fn.getDeclarationKind?.() && {
1141
+ declarationKind: fn.getDeclarationKind(),
1142
+ }),
1143
+ sourceLocation: {
1144
+ file: filePath,
1145
+ line: fn.getStartLineNumber(false),
1146
+ column: 0,
1147
+ },
1148
+ };
1149
+ }
1150
+ /**
1151
+ * Pre-infer ALL unannotated functions from a source file.
1152
+ * Used for imported files so the named-import filter can scope them.
1153
+ */
1154
+ inferAllUnannotatedFunctions(sourceFile, existingNodeTypes) {
1155
+ const allFunctions = extractFunctionLikes(sourceFile);
1156
+ const existingNames = new Set();
1157
+ for (const nt of existingNodeTypes) {
1158
+ existingNames.add(nt.name);
1159
+ existingNames.add(nt.functionName);
1160
+ }
1161
+ const inferred = [];
1162
+ for (const fn of allFunctions) {
1163
+ const fnName = fn.getName();
1164
+ if (!fnName)
1165
+ continue;
1166
+ // Skip if already known (annotated or from another source)
1167
+ if (existingNames.has(fnName))
1168
+ continue;
1169
+ // Must NOT have a valid @flowWeaver annotation
1170
+ if (this.hasFlowWeaverAnnotation(fn))
1171
+ continue;
1172
+ inferred.push(this.inferNodeTypeFromFunction(fn, fnName, sourceFile.getFilePath()));
1173
+ existingNames.add(fnName);
1174
+ }
1175
+ return inferred;
1176
+ }
1177
+ /**
1178
+ * Auto-infer node types from unannotated functions referenced by @node.
1179
+ *
1180
+ * When a workflow references a function via @node that has no @flowWeaver
1181
+ * nodeType annotation, we infer an expression node type from its TypeScript
1182
+ * signature. Phase 1: same-file functions only.
1183
+ */
1184
+ inferNodeTypesFromUnannotated(sourceFile, existingNodeTypes) {
1185
+ const allFunctions = extractFunctionLikes(sourceFile);
1186
+ // 1. Pre-scan workflows for @node references to collect referenced type names
1187
+ const referencedTypes = new Set();
1188
+ for (const fn of allFunctions) {
1189
+ const config = jsdocParser.parseWorkflow(fn, []);
1190
+ if (!config)
1191
+ continue;
1192
+ for (const inst of config.instances || []) {
1193
+ referencedTypes.add(inst.type);
1194
+ }
1195
+ }
1196
+ // 2. Find unresolved types: referenced but not in existingNodeTypes
1197
+ const existingNames = new Set();
1198
+ for (const nt of existingNodeTypes) {
1199
+ existingNames.add(nt.name);
1200
+ existingNames.add(nt.functionName);
1201
+ }
1202
+ const unresolvedTypes = new Set();
1203
+ for (const typeName of referencedTypes) {
1204
+ if (!existingNames.has(typeName)) {
1205
+ unresolvedTypes.add(typeName);
1206
+ }
1207
+ }
1208
+ if (unresolvedTypes.size === 0)
1209
+ return [];
1210
+ // 3. Match unresolved types to unannotated functions in the same file
1211
+ const inferredNodeTypes = [];
1212
+ const alreadyInferred = new Set();
1213
+ for (const unresolvedType of unresolvedTypes) {
1214
+ if (alreadyInferred.has(unresolvedType))
1215
+ continue;
1216
+ // Find matching function
1217
+ const matchedFn = allFunctions.find((fn) => {
1218
+ if (fn.getName() !== unresolvedType)
1219
+ return false;
1220
+ // Must NOT have a valid @flowWeaver annotation
1221
+ return !this.hasFlowWeaverAnnotation(fn);
1222
+ });
1223
+ if (!matchedFn)
1224
+ continue;
1225
+ inferredNodeTypes.push(this.inferNodeTypeFromFunction(matchedFn, unresolvedType, sourceFile.getFilePath()));
1226
+ alreadyInferred.add(unresolvedType);
1227
+ }
1228
+ return inferredNodeTypes;
1229
+ }
1230
+ /**
1231
+ * Expand a @map macro into synthetic node type, instances, connections, and scope.
1232
+ *
1233
+ * @map loop proc over scan.files
1234
+ *
1235
+ * Expands to:
1236
+ * - A synthetic MAP_ITERATOR node type for "loop" with scoped ports
1237
+ * - An instance "loop" of that synthetic type
1238
+ * - The child instance "proc" placed inside loop.iterate scope
1239
+ * - All scoped connections auto-generated
1240
+ * - Upstream connection from scan.files -> loop.items
1241
+ */
1242
+ expandMapMacro(mapConfig, instances, connections, scopes, availableNodeTypes, macros, errors, _warnings) {
1243
+ const { instanceId, childId, sourceNode, sourcePort } = mapConfig;
1244
+ const scopeName = 'iterate';
1245
+ // Find the child node instance (must already be declared via @node)
1246
+ const childInstance = instances.find((inst) => inst.id === childId);
1247
+ if (!childInstance) {
1248
+ errors.push(`@map "${instanceId}": child node "${childId}" not found. Declare it with @node before using @map.`);
1249
+ return;
1250
+ }
1251
+ // Find the child's node type to determine ports
1252
+ const childNodeType = availableNodeTypes.find((nt) => nt.name === childInstance.nodeType || nt.functionName === childInstance.nodeType);
1253
+ if (!childNodeType) {
1254
+ errors.push(`@map "${instanceId}": node type "${childInstance.nodeType}" for child "${childId}" not found.`);
1255
+ return;
1256
+ }
1257
+ // Determine input/output ports on the child
1258
+ // Auto-infer: first non-execute data input, first non-control-flow data output
1259
+ let inputPort = mapConfig.inputPort;
1260
+ let outputPort = mapConfig.outputPort;
1261
+ if (!inputPort) {
1262
+ const dataInputs = Object.entries(childNodeType.inputs).filter(([name, def]) => name !== 'execute' && def.dataType !== 'STEP');
1263
+ if (dataInputs.length === 0) {
1264
+ errors.push(`@map "${instanceId}": child node "${childId}" has no data input ports to receive items.`);
1265
+ return;
1266
+ }
1267
+ inputPort = dataInputs[0][0];
1268
+ }
1269
+ if (!outputPort) {
1270
+ const dataOutputs = Object.entries(childNodeType.outputs).filter(([name, def]) => name !== 'onSuccess' && name !== 'onFailure' && def.dataType !== 'STEP' && !def.isControlFlow && !def.failure);
1271
+ if (dataOutputs.length === 0) {
1272
+ errors.push(`@map "${instanceId}": child node "${childId}" has no data output ports for results.`);
1273
+ return;
1274
+ }
1275
+ outputPort = dataOutputs[0][0];
1276
+ }
1277
+ // Get type info from child ports for the synthetic type
1278
+ const childInputDef = childNodeType.inputs[inputPort];
1279
+ const childOutputDef = childNodeType.outputs[outputPort];
1280
+ // Create synthetic MAP_ITERATOR node type
1281
+ const syntheticTypeName = `__map_${instanceId}__`;
1282
+ const syntheticNodeType = {
1283
+ type: 'NodeType',
1284
+ name: syntheticTypeName,
1285
+ functionName: syntheticTypeName,
1286
+ variant: 'MAP_ITERATOR',
1287
+ isAsync: true,
1288
+ executeWhen: 'CONJUNCTION',
1289
+ hasSuccessPort: true,
1290
+ hasFailurePort: true,
1291
+ scope: scopeName,
1292
+ scopes: [scopeName],
1293
+ inputs: {
1294
+ execute: { dataType: 'STEP', label: 'Execute' },
1295
+ items: {
1296
+ dataType: 'ARRAY',
1297
+ label: 'Items',
1298
+ tsType: childInputDef?.tsType ? `(${childInputDef.tsType})[]` : 'unknown[]',
1299
+ },
1300
+ // Scoped INPUT ports (receive from children)
1301
+ success: { dataType: 'STEP', scope: scopeName },
1302
+ failure: { dataType: 'STEP', scope: scopeName },
1303
+ processed: {
1304
+ dataType: childOutputDef?.dataType || 'ANY',
1305
+ scope: scopeName,
1306
+ ...(childOutputDef?.tsType && { tsType: childOutputDef.tsType }),
1307
+ },
1308
+ },
1309
+ outputs: {
1310
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
1311
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
1312
+ results: {
1313
+ dataType: 'ARRAY',
1314
+ label: 'Results',
1315
+ tsType: childOutputDef?.tsType ? `(${childOutputDef.tsType})[]` : 'unknown[]',
1316
+ },
1317
+ // Scoped OUTPUT ports (send to children)
1318
+ start: { dataType: 'STEP', scope: scopeName },
1319
+ item: {
1320
+ dataType: childInputDef?.dataType || 'ANY',
1321
+ scope: scopeName,
1322
+ ...(childInputDef?.tsType && { tsType: childInputDef.tsType }),
1323
+ },
1324
+ },
1325
+ };
1326
+ // Add synthetic type to available types
1327
+ availableNodeTypes.push(syntheticNodeType);
1328
+ // Create instance for the map iterator
1329
+ const mapInstance = {
1330
+ type: 'NodeInstance',
1331
+ id: instanceId,
1332
+ nodeType: syntheticTypeName,
1333
+ };
1334
+ instances.push(mapInstance);
1335
+ // Move child instance into the scope
1336
+ childInstance.parent = { id: instanceId, scope: scopeName };
1337
+ // Generate scoped connections
1338
+ // loop.start:iterate -> proc.execute
1339
+ connections.push({
1340
+ type: 'Connection',
1341
+ from: { node: instanceId, port: 'start', scope: scopeName },
1342
+ to: { node: childId, port: 'execute', scope: scopeName },
1343
+ });
1344
+ // loop.item:iterate -> proc.<inputPort>
1345
+ connections.push({
1346
+ type: 'Connection',
1347
+ from: { node: instanceId, port: 'item', scope: scopeName },
1348
+ to: { node: childId, port: inputPort, scope: scopeName },
1349
+ });
1350
+ // proc.<outputPort> -> loop.processed:iterate
1351
+ connections.push({
1352
+ type: 'Connection',
1353
+ from: { node: childId, port: outputPort, scope: scopeName },
1354
+ to: { node: instanceId, port: 'processed', scope: scopeName },
1355
+ });
1356
+ // proc.onSuccess -> loop.success:iterate
1357
+ connections.push({
1358
+ type: 'Connection',
1359
+ from: { node: childId, port: 'onSuccess', scope: scopeName },
1360
+ to: { node: instanceId, port: 'success', scope: scopeName },
1361
+ });
1362
+ // proc.onFailure -> loop.failure:iterate
1363
+ connections.push({
1364
+ type: 'Connection',
1365
+ from: { node: childId, port: 'onFailure', scope: scopeName },
1366
+ to: { node: instanceId, port: 'failure', scope: scopeName },
1367
+ });
1368
+ // Generate upstream connection: source.port -> loop.items
1369
+ connections.push({
1370
+ type: 'Connection',
1371
+ from: { node: sourceNode, port: sourcePort },
1372
+ to: { node: instanceId, port: 'items' },
1373
+ });
1374
+ // Register scope
1375
+ scopes[`${instanceId}.${scopeName}`] = [childId];
1376
+ // Store macro for round-trip preservation
1377
+ macros.push({
1378
+ type: 'map',
1379
+ instanceId,
1380
+ childId,
1381
+ sourcePort: `${sourceNode}.${sourcePort}`,
1382
+ ...(mapConfig.inputPort && { inputPort: mapConfig.inputPort }),
1383
+ ...(mapConfig.outputPort && { outputPort: mapConfig.outputPort }),
1384
+ });
1385
+ }
1386
+ /**
1387
+ * Expand @path macros into multi-step execution routes with scope walking.
1388
+ * Processes all paths together for shared deduplication.
1389
+ */
1390
+ expandPathMacros(pathConfigs, instances, connections, availableNodeTypes, startPorts, exitPorts, macros, errors, warnings) {
1391
+ // Helper to find a node type by name or functionName
1392
+ const findNodeType = (nodeTypeName) => availableNodeTypes.find((nt) => nt.name === nodeTypeName || nt.functionName === nodeTypeName);
1393
+ // Helper to resolve instance → node type
1394
+ const getNodeType = (nodeId) => {
1395
+ const instance = instances.find((inst) => inst.id === nodeId);
1396
+ if (!instance)
1397
+ return undefined;
1398
+ return findNodeType(instance.nodeType);
1399
+ };
1400
+ // Helper to get output ports for a node (handling Start/Exit)
1401
+ const getOutputPorts = (nodeId) => {
1402
+ if (nodeId === 'Start')
1403
+ return startPorts;
1404
+ if (nodeId === 'Exit')
1405
+ return exitPorts;
1406
+ const nodeType = getNodeType(nodeId);
1407
+ return nodeType?.outputs || {};
1408
+ };
1409
+ // Helper to get input ports for a node (handling Start/Exit)
1410
+ const getInputPorts = (nodeId) => {
1411
+ if (nodeId === 'Exit')
1412
+ return exitPorts;
1413
+ if (nodeId === 'Start')
1414
+ return startPorts;
1415
+ const nodeType = getNodeType(nodeId);
1416
+ return nodeType?.inputs || {};
1417
+ };
1418
+ // Build a set of existing connection keys for deduplication
1419
+ const existingKeys = new Set();
1420
+ for (const conn of connections) {
1421
+ existingKeys.add(`${conn.from.node}.${conn.from.port}->${conn.to.node}.${conn.to.port}`);
1422
+ }
1423
+ const addConnection = (fromNode, fromPort, toNode, toPort) => {
1424
+ const key = `${fromNode}.${fromPort}->${toNode}.${toPort}`;
1425
+ if (existingKeys.has(key))
1426
+ return;
1427
+ existingKeys.add(key);
1428
+ connections.push({
1429
+ type: 'Connection',
1430
+ from: { node: fromNode, port: fromPort },
1431
+ to: { node: toNode, port: toPort },
1432
+ });
1433
+ };
1434
+ for (const pathConfig of pathConfigs) {
1435
+ const { steps } = pathConfig;
1436
+ if (steps.length < 2) {
1437
+ errors.push(`@path requires at least 2 steps, got ${steps.length}.`);
1438
+ continue;
1439
+ }
1440
+ // Validate all node references exist
1441
+ let valid = true;
1442
+ for (const step of steps) {
1443
+ if (step.node === 'Start' || step.node === 'Exit')
1444
+ continue;
1445
+ const instance = instances.find((inst) => inst.id === step.node);
1446
+ if (!instance) {
1447
+ errors.push(`@path: node "${step.node}" not found. Declare it with @node before using @path.`);
1448
+ valid = false;
1449
+ }
1450
+ }
1451
+ if (!valid)
1452
+ continue;
1453
+ // Generate connections for each consecutive pair
1454
+ for (let i = 0; i < steps.length - 1; i++) {
1455
+ const current = steps[i];
1456
+ const next = steps[i + 1];
1457
+ const currentId = current.node;
1458
+ const nextId = next.node;
1459
+ const route = current.route || 'ok';
1460
+ // Control flow connection
1461
+ if (currentId === 'Start') {
1462
+ addConnection('Start', 'execute', nextId, 'execute');
1463
+ }
1464
+ else if (nextId === 'Exit') {
1465
+ if (route === 'fail') {
1466
+ addConnection(currentId, 'onFailure', 'Exit', 'onFailure');
1467
+ }
1468
+ else {
1469
+ addConnection(currentId, 'onSuccess', 'Exit', 'onSuccess');
1470
+ }
1471
+ }
1472
+ else {
1473
+ if (route === 'fail') {
1474
+ addConnection(currentId, 'onFailure', nextId, 'execute');
1475
+ }
1476
+ else {
1477
+ addConnection(currentId, 'onSuccess', nextId, 'execute');
1478
+ }
1479
+ }
1480
+ // Data port scope walking (skip Exit — no data inputs to wire)
1481
+ if (nextId === 'Exit')
1482
+ continue;
1483
+ const nextInputs = getInputPorts(nextId);
1484
+ for (const [inputName] of Object.entries(nextInputs)) {
1485
+ if (isControlFlowPort(inputName))
1486
+ continue;
1487
+ // Walk backward through path steps to find nearest ancestor with same-name output
1488
+ for (let j = i; j >= 0; j--) {
1489
+ const ancestorId = steps[j].node;
1490
+ const ancestorOutputs = getOutputPorts(ancestorId);
1491
+ if (inputName in ancestorOutputs && !isControlFlowPort(inputName)) {
1492
+ addConnection(ancestorId, inputName, nextId, inputName);
1493
+ break;
1494
+ }
1495
+ }
1496
+ }
1497
+ }
1498
+ // Store macro for round-trip preservation
1499
+ macros.push({
1500
+ type: 'path',
1501
+ steps: steps.map(s => s.route ? { node: s.node, route: s.route } : { node: s.node }),
1502
+ });
1503
+ }
1504
+ }
1505
+ /**
1506
+ * Generate automatic connections for @autoConnect workflows.
1507
+ * Wires nodes in declaration order as a linear pipeline:
1508
+ * Start -> first node -> second node -> ... -> last node -> Exit
1509
+ *
1510
+ * For each consecutive pair:
1511
+ * - Connect execute flow: previous.onSuccess -> next.execute
1512
+ * - Connect data ports where output name matches input name
1513
+ * For first node: Start.execute -> first.execute + match Start data ports to first inputs
1514
+ * For last node: last.onSuccess -> Exit.execute + match last outputs to Exit ports
1515
+ */
1516
+ generateAutoConnections(instances, availableNodeTypes, startPorts, exitPorts) {
1517
+ const connections = [];
1518
+ // Helper to find a node type by name or functionName
1519
+ const findNodeType = (nodeTypeName) => availableNodeTypes.find((nt) => nt.name === nodeTypeName || nt.functionName === nodeTypeName);
1520
+ // Connect Start -> first node
1521
+ if (instances.length > 0) {
1522
+ const firstInstance = instances[0];
1523
+ const firstNodeType = findNodeType(firstInstance.nodeType);
1524
+ // Start.execute -> first.execute (execution flow)
1525
+ connections.push({
1526
+ type: 'Connection',
1527
+ from: { node: 'Start', port: 'execute' },
1528
+ to: { node: firstInstance.id, port: 'execute' },
1529
+ });
1530
+ // Match Start data ports to first node's data inputs
1531
+ if (firstNodeType) {
1532
+ for (const [portName, portDef] of Object.entries(startPorts)) {
1533
+ if (portDef.dataType === 'STEP')
1534
+ continue; // Skip control flow
1535
+ if (portName in firstNodeType.inputs && !isControlFlowPort(portName)) {
1536
+ connections.push({
1537
+ type: 'Connection',
1538
+ from: { node: 'Start', port: portName },
1539
+ to: { node: firstInstance.id, port: portName },
1540
+ });
1541
+ }
1542
+ }
1543
+ }
1544
+ }
1545
+ // Connect consecutive nodes
1546
+ for (let i = 0; i < instances.length - 1; i++) {
1547
+ const current = instances[i];
1548
+ const next = instances[i + 1];
1549
+ const currentNodeType = findNodeType(current.nodeType);
1550
+ const nextNodeType = findNodeType(next.nodeType);
1551
+ // current.onSuccess -> next.execute (execution flow)
1552
+ connections.push({
1553
+ type: 'Connection',
1554
+ from: { node: current.id, port: 'onSuccess' },
1555
+ to: { node: next.id, port: 'execute' },
1556
+ });
1557
+ // Match data ports: current outputs -> next inputs (by matching port names)
1558
+ if (currentNodeType && nextNodeType) {
1559
+ for (const [outputName, outputDef] of Object.entries(currentNodeType.outputs)) {
1560
+ if (outputDef.dataType === 'STEP' || isControlFlowPort(outputName))
1561
+ continue;
1562
+ if (outputName in nextNodeType.inputs && !isControlFlowPort(outputName)) {
1563
+ connections.push({
1564
+ type: 'Connection',
1565
+ from: { node: current.id, port: outputName },
1566
+ to: { node: next.id, port: outputName },
1567
+ });
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+ // Connect last node -> Exit
1573
+ if (instances.length > 0) {
1574
+ const lastInstance = instances[instances.length - 1];
1575
+ const lastNodeType = findNodeType(lastInstance.nodeType);
1576
+ // last.onSuccess -> Exit.onSuccess (execution flow)
1577
+ connections.push({
1578
+ type: 'Connection',
1579
+ from: { node: lastInstance.id, port: 'onSuccess' },
1580
+ to: { node: 'Exit', port: 'onSuccess' },
1581
+ });
1582
+ // Match last node's data outputs to Exit data ports
1583
+ if (lastNodeType) {
1584
+ for (const [portName, portDef] of Object.entries(exitPorts)) {
1585
+ if (portDef.dataType === 'STEP' || portDef.isControlFlow)
1586
+ continue;
1587
+ if (lastNodeType.outputs[portName] && !isControlFlowPort(portName)) {
1588
+ connections.push({
1589
+ type: 'Connection',
1590
+ from: { node: lastInstance.id, port: portName },
1591
+ to: { node: 'Exit', port: portName },
1592
+ });
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+ return connections;
1598
+ }
1599
+ parseStartPorts(fn, config) {
1600
+ const ports = {};
1601
+ const params = fn.getParameters();
1602
+ // New architecture: first parameter should be execute: boolean
1603
+ // Second parameter is the params object with data
1604
+ if (params.length === 0) {
1605
+ // No parameters - just return execute port
1606
+ ports.execute = { dataType: 'STEP', label: 'Execute' };
1607
+ return ports;
1608
+ }
1609
+ // Check if first parameter is execute: boolean
1610
+ const firstParam = params[0];
1611
+ const firstParamName = firstParam.getName();
1612
+ const firstParamType = firstParam.getType();
1613
+ const firstParamTypeText = firstParamType.getText();
1614
+ if (firstParamName === 'execute' && firstParamTypeText === 'boolean') {
1615
+ // Correct new format: first param is execute
1616
+ // Check if JSDoc has explicit metadata override for execute port (from @param annotation)
1617
+ if (config?.startPorts && config.startPorts['execute']) {
1618
+ ports.execute = {
1619
+ dataType: 'STEP',
1620
+ tsType: 'boolean',
1621
+ ...config.startPorts['execute'],
1622
+ };
1623
+ }
1624
+ else {
1625
+ ports.execute = { dataType: 'STEP', tsType: 'boolean', label: 'Execute' };
1626
+ }
1627
+ // Extract data ports from second parameter if it exists
1628
+ if (params.length > 1) {
1629
+ const dataParam = params[1];
1630
+ const dataParamType = dataParam.getType();
1631
+ const properties = dataParamType.getProperties();
1632
+ properties.forEach((prop) => {
1633
+ const propName = prop.getName();
1634
+ const propType = prop.getTypeAtLocation(dataParam);
1635
+ const portType = this.inferPortType(propType);
1636
+ const propTypeText = propType.getText();
1637
+ // Extract schema for complex types (interfaces/objects)
1638
+ const tsSchema = portType === 'OBJECT' ? this.extractTypeSchema(propType) : undefined;
1639
+ // Check if JSDoc has explicit metadata override for this start port (from @param annotation)
1640
+ // Always prefer ts-morph inferred type over JSDoc regex-based inference
1641
+ // (JSDoc @param type inference uses regex which can fail for complex types)
1642
+ const startPortConfig = config?.startPorts?.[propName];
1643
+ ports[propName] = {
1644
+ dataType: portType,
1645
+ label: startPortConfig?.label || this.capitalize(propName),
1646
+ ...(startPortConfig?.metadata && { metadata: startPortConfig.metadata }),
1647
+ // Include original TS type for rich type display
1648
+ ...(propTypeText && { tsType: propTypeText }),
1649
+ // Include schema breakdown for complex types
1650
+ ...(tsSchema && Object.keys(tsSchema).length > 0 && { tsSchema }),
1651
+ };
1652
+ });
1653
+ }
1654
+ }
1655
+ else {
1656
+ // Old format detected - reject it
1657
+ throw new Error(`Invalid node type function signature for "${fn.getName()}". ` +
1658
+ `Expected first parameter to be "execute: boolean", but got "${firstParamName}: ${firstParamTypeText}". ` +
1659
+ `Correct format: function ${fn.getName()}(execute: boolean, data: {...}) { ... }`);
1660
+ }
1661
+ // Assign implicit port orders with mandatory port precedence
1662
+ assignImplicitPortOrders(ports);
1663
+ return ports;
1664
+ }
1665
+ parseExitPorts(fn, config) {
1666
+ const ports = {};
1667
+ let returnType = fn.getReturnType();
1668
+ // If return type is a Promise, extract the type parameter
1669
+ const typeText = returnType?.getText();
1670
+ const sourceFile = fn.getSourceFile();
1671
+ const filePath = sourceFile?.getFilePath() || 'unknown file';
1672
+ const fileName = path.basename(filePath);
1673
+ if (!typeText || typeText === 'void') {
1674
+ console.warn(`[PARSER] Could not determine return type for function "${fn.getName()}" in ${fileName}.\n` +
1675
+ ` Add an explicit return type like: Promise<{ onSuccess: boolean; onFailure: boolean }>`);
1676
+ return ports;
1677
+ }
1678
+ if (typeText.startsWith('Promise<')) {
1679
+ const typeArgs = returnType.getTypeArguments();
1680
+ if (typeArgs && typeArgs.length > 0) {
1681
+ returnType = typeArgs[0];
1682
+ }
1683
+ }
1684
+ const properties = returnType.getProperties();
1685
+ properties.forEach((prop) => {
1686
+ const propName = prop.getName();
1687
+ // Always infer from TypeScript signature, use @returns annotation only for metadata
1688
+ // onSuccess/onFailure are always STEP ports regardless of annotations
1689
+ if (propName === 'onSuccess' || propName === 'onFailure') {
1690
+ const returnPortConfig = config?.returnPorts?.[propName];
1691
+ ports[propName] = {
1692
+ dataType: 'STEP',
1693
+ label: returnPortConfig?.label || (propName === 'onSuccess' ? 'On Success' : 'On Failure'),
1694
+ isControlFlow: true,
1695
+ ...(returnPortConfig?.metadata && { metadata: returnPortConfig.metadata }),
1696
+ };
1697
+ }
1698
+ else {
1699
+ // Auto-infer type from TypeScript signature (more accurate than JSDoc regex)
1700
+ const propType = prop.getTypeAtLocation(fn.getTypeResolutionNode());
1701
+ const portType = this.inferPortType(propType);
1702
+ const propTypeText = propType.getText();
1703
+ // Extract schema for complex types (interfaces/objects)
1704
+ const tsSchema = portType === 'OBJECT' ? this.extractTypeSchema(propType) : undefined;
1705
+ const returnPortConfig = config?.returnPorts?.[propName];
1706
+ ports[propName] = {
1707
+ dataType: portType,
1708
+ label: returnPortConfig?.label || this.capitalize(propName),
1709
+ ...(returnPortConfig?.metadata && { metadata: returnPortConfig.metadata }),
1710
+ // Include original TS type for rich type display (e.g., "ResearchReport" instead of "object")
1711
+ ...(propTypeText && propTypeText !== portType.toLowerCase() && { tsType: propTypeText }),
1712
+ // Include schema breakdown for complex types
1713
+ ...(tsSchema && Object.keys(tsSchema).length > 0 && { tsSchema }),
1714
+ };
1715
+ }
1716
+ });
1717
+ // Assign implicit port orders with mandatory port precedence
1718
+ assignImplicitPortOrders(ports);
1719
+ return ports;
1720
+ }
1721
+ /**
1722
+ * Extract schema breakdown for complex types (interfaces/objects).
1723
+ * Returns a map of property names to their TypeScript type strings.
1724
+ */
1725
+ extractTypeSchema(tsType) {
1726
+ const schema = {};
1727
+ const properties = tsType.getProperties();
1728
+ if (!properties || properties.length === 0) {
1729
+ return undefined;
1730
+ }
1731
+ for (const prop of properties) {
1732
+ const propName = prop.getName();
1733
+ // Skip internal/private properties
1734
+ if (propName.startsWith('_'))
1735
+ continue;
1736
+ const propType = prop.getValueDeclaration()?.getType();
1737
+ if (propType) {
1738
+ schema[propName] = propType.getText();
1739
+ }
1740
+ else {
1741
+ // Fallback: try to get type from declarations
1742
+ const declarations = prop.getDeclarations();
1743
+ if (declarations && declarations.length > 0) {
1744
+ const decl = declarations[0];
1745
+ schema[propName] = decl.getType().getText();
1746
+ }
1747
+ }
1748
+ }
1749
+ return Object.keys(schema).length > 0 ? schema : undefined;
1750
+ }
1751
+ inferPortType(tsType) {
1752
+ const typeText = tsType.getText();
1753
+ // Delegate to inferDataTypeFromTS for consistent type mapping
1754
+ // This handles all cases: primitives, any, unknown, never, arrays, functions, etc.
1755
+ return inferDataTypeFromTS(typeText);
1756
+ }
1757
+ /**
1758
+ * Check if a function has a valid @flowWeaver annotation (nodeType, workflow, or pattern).
1759
+ * Avoids false positives from file-level JSDoc that mentions @flowWeaver in description text.
1760
+ */
1761
+ hasFlowWeaverAnnotation(fn) {
1762
+ const validTypes = new Set(['nodeType', 'workflow', 'pattern']);
1763
+ return fn.getJsDocs().some((doc) => doc.getTags().some((t) => {
1764
+ if (t.getTagName() !== 'flowWeaver')
1765
+ return false;
1766
+ const comment = t.getCommentText?.()?.trim() || '';
1767
+ return validTypes.has(comment.split(/\s/)[0]);
1768
+ }));
1769
+ }
1770
+ /**
1771
+ * Generate an annotation suggestion for ghost-text autocomplete.
1772
+ * Analyzes the function nearest to cursorLine, diffs inferred ports against
1773
+ * any existing @flowWeaver annotation, and returns only the missing lines.
1774
+ */
1775
+ generateAnnotationSuggestion(content, cursorLine, virtualPath = 'virtual.ts') {
1776
+ // Create virtual SourceFile
1777
+ const existingFile = this.project.getSourceFile(virtualPath);
1778
+ if (existingFile) {
1779
+ this.project.removeSourceFile(existingFile);
1780
+ }
1781
+ const sourceFile = this.project.createSourceFile(virtualPath, content, { overwrite: true });
1782
+ try {
1783
+ const allFunctions = extractFunctionLikes(sourceFile);
1784
+ if (allFunctions.length === 0)
1785
+ return null;
1786
+ // Find the function nearest to cursorLine (below or containing the cursor)
1787
+ // cursorLine is 0-based; getStartLineNumber() is 1-based
1788
+ let targetFn = null;
1789
+ let bestDistance = Infinity;
1790
+ for (const fn of allFunctions) {
1791
+ const fnLine = fn.getStartLineNumber(false) - 1; // 0-based
1792
+ // Prefer functions at or below the cursor
1793
+ const distance = fnLine >= cursorLine ? fnLine - cursorLine : (cursorLine - fnLine) + 1000;
1794
+ if (distance < bestDistance) {
1795
+ bestDistance = distance;
1796
+ targetFn = fn;
1797
+ }
1798
+ }
1799
+ if (!targetFn)
1800
+ return null;
1801
+ const fnName = targetFn.getName() || 'anonymous';
1802
+ const fnStartLine = targetFn.getStartLineNumber(false) - 1; // 0-based
1803
+ // Don't suggest if cursor is too far from the function (more than 30 lines above)
1804
+ if (cursorLine < fnStartLine - 30)
1805
+ return null;
1806
+ // Check existing JSDoc state
1807
+ const hasAnnotation = this.hasFlowWeaverAnnotation(targetFn);
1808
+ const hasAnyJsDoc = targetFn.getJsDocs().length > 0;
1809
+ // If function has a JSDoc but NOT a @flowWeaver annotation, don't suggest
1810
+ // a competing JSDoc block — the user has an intentional regular JSDoc
1811
+ if (hasAnyJsDoc && !hasAnnotation)
1812
+ return null;
1813
+ // Infer full node type from function signature
1814
+ const inferred = this.inferNodeTypeFromFunction(targetFn, fnName, virtualPath);
1815
+ // Extract @param descriptions from existing JSDoc (if any)
1816
+ const paramDescriptions = new Map();
1817
+ for (const doc of targetFn.getJsDocs()) {
1818
+ for (const tag of doc.getTags()) {
1819
+ if (tag.getTagName() === 'param') {
1820
+ const comment = tag.getCommentText?.()?.trim() || '';
1821
+ // Extract param name and description: "{type} name - desc" or "name - desc" or "name desc"
1822
+ const paramMatch = comment.match(/^(?:\{[^}]*\}\s+)?(\w+)(?:\s*-\s*|\s+)(.+)/);
1823
+ if (paramMatch) {
1824
+ paramDescriptions.set(paramMatch[1], paramMatch[2]);
1825
+ }
1826
+ }
1827
+ }
1828
+ }
1829
+ // Merge @param descriptions into inferred port labels
1830
+ for (const [portName, portDef] of Object.entries(inferred.inputs)) {
1831
+ const desc = paramDescriptions.get(portName);
1832
+ if (desc) {
1833
+ portDef.label = desc;
1834
+ }
1835
+ }
1836
+ // Parse existing JSDoc to find what's already annotated
1837
+ const existingPorts = this.extractExistingAnnotatedPorts(targetFn);
1838
+ // Build missing port lines
1839
+ const missingLines = [];
1840
+ // Filter out mandatory ports (execute, onSuccess, onFailure) from suggestions
1841
+ for (const [portName, portDef] of Object.entries(inferred.inputs)) {
1842
+ if (isControlFlowPort(portName))
1843
+ continue;
1844
+ if (existingPorts.inputs.has(portName))
1845
+ continue;
1846
+ missingLines.push(` * ${generateJSDocPortTag(portName, portDef, 'input')}`);
1847
+ }
1848
+ for (const [portName, portDef] of Object.entries(inferred.outputs)) {
1849
+ if (isControlFlowPort(portName))
1850
+ continue;
1851
+ if (existingPorts.outputs.has(portName))
1852
+ continue;
1853
+ missingLines.push(` * ${generateJSDocPortTag(portName, portDef, 'output')}`);
1854
+ }
1855
+ if (hasAnnotation) {
1856
+ // Check if this is a workflow block — if so, suggest missing connections
1857
+ const isWorkflow = this.isWorkflowBlock(targetFn);
1858
+ if (isWorkflow) {
1859
+ const connectionLines = this.generateWorkflowStructureSuggestion(targetFn, sourceFile);
1860
+ missingLines.push(...connectionLines);
1861
+ }
1862
+ // Partial JSDoc: suggest only missing ports / connections
1863
+ if (missingLines.length === 0)
1864
+ return null;
1865
+ // Find the insertion point: just before the closing */
1866
+ const lines = content.split('\n');
1867
+ let jsDocEndLine = -1;
1868
+ for (let i = fnStartLine - 1; i >= 0; i--) {
1869
+ if (lines[i].includes('*/')) {
1870
+ jsDocEndLine = i;
1871
+ break;
1872
+ }
1873
+ }
1874
+ if (jsDocEndLine < 0)
1875
+ return null;
1876
+ const text = missingLines.join('\n') + '\n';
1877
+ return {
1878
+ text,
1879
+ insertLine: jsDocEndLine,
1880
+ replaceLinesCount: 0,
1881
+ };
1882
+ }
1883
+ // No @flowWeaver JSDoc — check if user just typed "/**" on the cursor line
1884
+ const lines = content.split('\n');
1885
+ const cursorLineText = lines[cursorLine] || '';
1886
+ if (/^\s*\/\*\*\s*$/.test(cursorLineText)) {
1887
+ // User typed "/**" — generate only the continuation lines after it
1888
+ const continuationLines = [
1889
+ ` * @flowWeaver nodeType ${fnName}`,
1890
+ ...(inferred.expression ? [' * @expression'] : []),
1891
+ ...missingLines,
1892
+ ' */',
1893
+ ];
1894
+ const text = continuationLines.join('\n') + '\n';
1895
+ return {
1896
+ text,
1897
+ insertLine: cursorLine + 1,
1898
+ replaceLinesCount: 0,
1899
+ };
1900
+ }
1901
+ // Generate full annotation block
1902
+ const allLines = [
1903
+ '/**',
1904
+ ` * @flowWeaver nodeType ${fnName}`,
1905
+ ...(inferred.expression ? [' * @expression'] : []),
1906
+ ...missingLines,
1907
+ ' */',
1908
+ ];
1909
+ const text = allLines.join('\n') + '\n';
1910
+ // Insert on the line above the function
1911
+ return {
1912
+ text,
1913
+ insertLine: fnStartLine,
1914
+ replaceLinesCount: 0,
1915
+ };
1916
+ }
1917
+ finally {
1918
+ // Clean up virtual source file
1919
+ const sf = this.project.getSourceFile(virtualPath);
1920
+ if (sf)
1921
+ this.project.removeSourceFile(sf);
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Check if a function's JSDoc marks it as a workflow (vs nodeType).
1926
+ */
1927
+ isWorkflowBlock(fn) {
1928
+ for (const doc of fn.getJsDocs()) {
1929
+ for (const tag of doc.getTags()) {
1930
+ if (tag.getTagName() !== 'flowWeaver')
1931
+ continue;
1932
+ const comment = tag.getCommentText?.()?.trim() || '';
1933
+ const firstWord = comment.split(/\s/)[0];
1934
+ // 'workflow' explicitly, or bare @flowWeaver (no qualifier), or named workflow
1935
+ if (firstWord === 'workflow' || firstWord === '' || (firstWord !== 'nodeType' && firstWord !== 'pattern')) {
1936
+ return true;
1937
+ }
1938
+ }
1939
+ }
1940
+ return false;
1941
+ }
1942
+ /**
1943
+ * Generate missing @connect suggestions for a workflow block.
1944
+ * Finds @node declarations, resolves their types, and suggests connections
1945
+ * for matching port names that aren't already wired.
1946
+ */
1947
+ generateWorkflowStructureSuggestion(fn, sourceFile) {
1948
+ // Extract @node declarations: { nodeId -> nodeTypeName }
1949
+ const nodeDecls = new Map();
1950
+ // Extract existing @connect lines: set of "sourceNode.sourcePort->targetNode.targetPort"
1951
+ const existingConnections = new Set();
1952
+ for (const doc of fn.getJsDocs()) {
1953
+ for (const tag of doc.getTags()) {
1954
+ const tagName = tag.getTagName();
1955
+ const comment = tag.getCommentText?.()?.trim() || '';
1956
+ if (tagName === 'node') {
1957
+ const nodeMatch = comment.match(/^(\w+)\s+(\w+)/);
1958
+ if (nodeMatch) {
1959
+ nodeDecls.set(nodeMatch[1], nodeMatch[2]);
1960
+ }
1961
+ }
1962
+ else if (tagName === 'connect') {
1963
+ const connMatch = comment.match(/^(\w+)\.(\w+)\s*->\s*(\w+)\.(\w+)/);
1964
+ if (connMatch) {
1965
+ existingConnections.add(`${connMatch[1]}.${connMatch[2]}->${connMatch[3]}.${connMatch[4]}`);
1966
+ }
1967
+ }
1968
+ }
1969
+ }
1970
+ if (nodeDecls.size < 2)
1971
+ return [];
1972
+ // Resolve node types from the same file
1973
+ const allFunctions = extractFunctionLikes(sourceFile);
1974
+ const resolvedTypes = new Map();
1975
+ for (const [nodeId, typeName] of nodeDecls) {
1976
+ const matchedFn = allFunctions.find((f) => f.getName() === typeName);
1977
+ if (matchedFn) {
1978
+ resolvedTypes.set(nodeId, this.inferNodeTypeFromFunction(matchedFn, typeName, sourceFile.getFilePath()));
1979
+ }
1980
+ }
1981
+ // Find matching unconnected port pairs
1982
+ const suggestions = [];
1983
+ const nodeIds = [...nodeDecls.keys()];
1984
+ for (let i = 0; i < nodeIds.length; i++) {
1985
+ for (let j = 0; j < nodeIds.length; j++) {
1986
+ if (i === j)
1987
+ continue;
1988
+ const srcId = nodeIds[i];
1989
+ const tgtId = nodeIds[j];
1990
+ const srcType = resolvedTypes.get(srcId);
1991
+ const tgtType = resolvedTypes.get(tgtId);
1992
+ if (!srcType || !tgtType)
1993
+ continue;
1994
+ for (const [outputName, outputDef] of Object.entries(srcType.outputs)) {
1995
+ if (isControlFlowPort(outputName))
1996
+ continue;
1997
+ if (outputDef.dataType === 'STEP')
1998
+ continue;
1999
+ // Check if target has a matching input with the same name
2000
+ if (outputName in tgtType.inputs && !isControlFlowPort(outputName)) {
2001
+ const connKey = `${srcId}.${outputName}->${tgtId}.${outputName}`;
2002
+ if (!existingConnections.has(connKey)) {
2003
+ suggestions.push(` * @connect ${srcId}.${outputName} -> ${tgtId}.${outputName}`);
2004
+ existingConnections.add(connKey); // prevent duplicates
2005
+ }
2006
+ }
2007
+ }
2008
+ }
2009
+ }
2010
+ return suggestions;
2011
+ }
2012
+ /**
2013
+ * Extract port names that are already annotated in a function's JSDoc.
2014
+ * Returns sets of input and output port names found in existing annotations.
2015
+ */
2016
+ extractExistingAnnotatedPorts(fn) {
2017
+ const inputs = new Set();
2018
+ const outputs = new Set();
2019
+ for (const doc of fn.getJsDocs()) {
2020
+ for (const tag of doc.getTags()) {
2021
+ const tagName = tag.getTagName();
2022
+ const comment = tag.getCommentText?.()?.trim() || '';
2023
+ // Extract port name: first word, possibly wrapped in brackets [name] or [name=default]
2024
+ const nameMatch = comment.match(/^\[?(\w+)/);
2025
+ if (!nameMatch)
2026
+ continue;
2027
+ const portName = nameMatch[1];
2028
+ if (tagName === 'input' || tagName === 'step') {
2029
+ inputs.add(portName);
2030
+ }
2031
+ else if (tagName === 'output') {
2032
+ outputs.add(portName);
2033
+ }
2034
+ }
2035
+ }
2036
+ return { inputs, outputs };
2037
+ }
2038
+ capitalize(str) {
2039
+ return str.charAt(0).toUpperCase() + str.slice(1);
2040
+ }
2041
+ }
2042
+ export const parser = new AnnotationParser();
2043
+ /**
2044
+ * Resolve npm node types by re-reading their .d.ts files.
2045
+ * This fills in the full port information that isn't stored in @fwImport annotations.
2046
+ *
2047
+ * When workflows are parsed, npm node types from @fwImport annotations only contain
2048
+ * minimal stub information (name, functionName, importSource). This function re-resolves
2049
+ * the full port definitions from the actual .d.ts files of the npm packages.
2050
+ *
2051
+ * @param ast - The workflow AST with potentially stub npm node types
2052
+ * @param workdir - Directory to search for node_modules (typically the workflow file's directory)
2053
+ * @returns Updated AST with fully resolved npm node types
2054
+ */
2055
+ export function resolveNpmNodeTypes(ast, workdir) {
2056
+ if (!ast.nodeTypes || ast.nodeTypes.length === 0) {
2057
+ return ast;
2058
+ }
2059
+ const resolvedNodeTypes = ast.nodeTypes.map((nodeType) => {
2060
+ // Only resolve npm node types (those with importSource)
2061
+ if (!nodeType.importSource) {
2062
+ return nodeType;
2063
+ }
2064
+ // Get the full node type from the .d.ts file
2065
+ const packageExports = getPackageExports(nodeType.importSource, workdir);
2066
+ const matchingExport = packageExports.find((exp) => exp.name === nodeType.name || exp.function === nodeType.functionName);
2067
+ if (!matchingExport) {
2068
+ // Can't resolve - keep stub (will show only result port)
2069
+ return nodeType;
2070
+ }
2071
+ // Convert TNpmNodeType ports to TNodeTypeAST inputs/outputs
2072
+ const inputs = {};
2073
+ const outputs = {};
2074
+ for (const port of matchingExport.ports) {
2075
+ if (port.direction === 'INPUT') {
2076
+ inputs[port.name] = {
2077
+ dataType: port.type,
2078
+ label: port.defaultLabel,
2079
+ };
2080
+ }
2081
+ else if (port.direction === 'OUTPUT') {
2082
+ outputs[port.name] = {
2083
+ dataType: port.type,
2084
+ label: port.defaultLabel,
2085
+ };
2086
+ }
2087
+ }
2088
+ return {
2089
+ ...nodeType,
2090
+ inputs,
2091
+ outputs,
2092
+ isAsync: matchingExport.synchronicity === 'ASYNC',
2093
+ };
2094
+ });
2095
+ return {
2096
+ ...ast,
2097
+ nodeTypes: resolvedNodeTypes,
2098
+ };
2099
+ }
2100
+ //# sourceMappingURL=parser.js.map