@foresthubai/workflow-core 0.3.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 (339) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +14 -0
  3. package/README.md +63 -0
  4. package/dist/api/index.d.ts +7 -0
  5. package/dist/api/index.d.ts.map +1 -0
  6. package/dist/api/index.js +2 -0
  7. package/dist/api/index.js.map +1 -0
  8. package/dist/api/workflow.d.ts +607 -0
  9. package/dist/api/workflow.d.ts.map +1 -0
  10. package/dist/api/workflow.js +6 -0
  11. package/dist/api/workflow.js.map +1 -0
  12. package/dist/channel/Channel.d.ts +10 -0
  13. package/dist/channel/Channel.d.ts.map +1 -0
  14. package/dist/channel/Channel.js +2 -0
  15. package/dist/channel/Channel.js.map +1 -0
  16. package/dist/channel/ChannelDefinition.d.ts +27 -0
  17. package/dist/channel/ChannelDefinition.d.ts.map +1 -0
  18. package/dist/channel/ChannelDefinition.js +50 -0
  19. package/dist/channel/ChannelDefinition.js.map +1 -0
  20. package/dist/channel/index.d.ts +7 -0
  21. package/dist/channel/index.d.ts.map +1 -0
  22. package/dist/channel/index.js +4 -0
  23. package/dist/channel/index.js.map +1 -0
  24. package/dist/channel/serialization.d.ts +17 -0
  25. package/dist/channel/serialization.d.ts.map +1 -0
  26. package/dist/channel/serialization.js +63 -0
  27. package/dist/channel/serialization.js.map +1 -0
  28. package/dist/deploy/index.d.ts +2 -0
  29. package/dist/deploy/index.d.ts.map +1 -0
  30. package/dist/deploy/index.js +2 -0
  31. package/dist/deploy/index.js.map +1 -0
  32. package/dist/deploy/requirements.d.ts +17 -0
  33. package/dist/deploy/requirements.d.ts.map +1 -0
  34. package/dist/deploy/requirements.js +41 -0
  35. package/dist/deploy/requirements.js.map +1 -0
  36. package/dist/diagnostics/__fixtures__/diagnosticFixtures.d.ts +28 -0
  37. package/dist/diagnostics/__fixtures__/diagnosticFixtures.d.ts.map +1 -0
  38. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js +125 -0
  39. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js.map +1 -0
  40. package/dist/diagnostics/diagnostics.d.ts +128 -0
  41. package/dist/diagnostics/diagnostics.d.ts.map +1 -0
  42. package/dist/diagnostics/diagnostics.js +783 -0
  43. package/dist/diagnostics/diagnostics.js.map +1 -0
  44. package/dist/diagnostics/index.d.ts +3 -0
  45. package/dist/diagnostics/index.d.ts.map +1 -0
  46. package/dist/diagnostics/index.js +2 -0
  47. package/dist/diagnostics/index.js.map +1 -0
  48. package/dist/edge/Edge.d.ts +22 -0
  49. package/dist/edge/Edge.d.ts.map +1 -0
  50. package/dist/edge/Edge.js +2 -0
  51. package/dist/edge/Edge.js.map +1 -0
  52. package/dist/edge/EdgeDefinition.d.ts +10 -0
  53. package/dist/edge/EdgeDefinition.d.ts.map +1 -0
  54. package/dist/edge/EdgeDefinition.js +36 -0
  55. package/dist/edge/EdgeDefinition.js.map +1 -0
  56. package/dist/edge/EdgeType.d.ts +6 -0
  57. package/dist/edge/EdgeType.d.ts.map +1 -0
  58. package/dist/edge/EdgeType.js +7 -0
  59. package/dist/edge/EdgeType.js.map +1 -0
  60. package/dist/edge/index.d.ts +6 -0
  61. package/dist/edge/index.d.ts.map +1 -0
  62. package/dist/edge/index.js +6 -0
  63. package/dist/edge/index.js.map +1 -0
  64. package/dist/edge/serialization.d.ts +18 -0
  65. package/dist/edge/serialization.d.ts.map +1 -0
  66. package/dist/edge/serialization.js +76 -0
  67. package/dist/edge/serialization.js.map +1 -0
  68. package/dist/expression/index.d.ts +5 -0
  69. package/dist/expression/index.d.ts.map +1 -0
  70. package/dist/expression/index.js +3 -0
  71. package/dist/expression/index.js.map +1 -0
  72. package/dist/expression/parser.d.ts +14 -0
  73. package/dist/expression/parser.d.ts.map +1 -0
  74. package/dist/expression/parser.js +309 -0
  75. package/dist/expression/parser.js.map +1 -0
  76. package/dist/expression/types.d.ts +11 -0
  77. package/dist/expression/types.d.ts.map +1 -0
  78. package/dist/expression/types.js +20 -0
  79. package/dist/expression/types.js.map +1 -0
  80. package/dist/function/FunctionDeclaration.d.ts +43 -0
  81. package/dist/function/FunctionDeclaration.d.ts.map +1 -0
  82. package/dist/function/FunctionDeclaration.js +17 -0
  83. package/dist/function/FunctionDeclaration.js.map +1 -0
  84. package/dist/function/index.d.ts +4 -0
  85. package/dist/function/index.d.ts.map +1 -0
  86. package/dist/function/index.js +3 -0
  87. package/dist/function/index.js.map +1 -0
  88. package/dist/function/serialization.d.ts +20 -0
  89. package/dist/function/serialization.d.ts.map +1 -0
  90. package/dist/function/serialization.js +26 -0
  91. package/dist/function/serialization.js.map +1 -0
  92. package/dist/id/index.d.ts +7 -0
  93. package/dist/id/index.d.ts.map +1 -0
  94. package/dist/id/index.js +9 -0
  95. package/dist/id/index.js.map +1 -0
  96. package/dist/index.d.ts +11 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +14 -0
  99. package/dist/index.js.map +1 -0
  100. package/dist/memory/Memory.d.ts +12 -0
  101. package/dist/memory/Memory.d.ts.map +1 -0
  102. package/dist/memory/Memory.js +2 -0
  103. package/dist/memory/Memory.js.map +1 -0
  104. package/dist/memory/MemoryDefinition.d.ts +16 -0
  105. package/dist/memory/MemoryDefinition.d.ts.map +1 -0
  106. package/dist/memory/MemoryDefinition.js +2 -0
  107. package/dist/memory/MemoryDefinition.js.map +1 -0
  108. package/dist/memory/MemoryFileDefinition.d.ts +8 -0
  109. package/dist/memory/MemoryFileDefinition.d.ts.map +1 -0
  110. package/dist/memory/MemoryFileDefinition.js +36 -0
  111. package/dist/memory/MemoryFileDefinition.js.map +1 -0
  112. package/dist/memory/MemoryRegistry.d.ts +17 -0
  113. package/dist/memory/MemoryRegistry.d.ts.map +1 -0
  114. package/dist/memory/MemoryRegistry.js +29 -0
  115. package/dist/memory/MemoryRegistry.js.map +1 -0
  116. package/dist/memory/VectorDatabaseDefinition.d.ts +7 -0
  117. package/dist/memory/VectorDatabaseDefinition.d.ts.map +1 -0
  118. package/dist/memory/VectorDatabaseDefinition.js +20 -0
  119. package/dist/memory/VectorDatabaseDefinition.js.map +1 -0
  120. package/dist/memory/index.d.ts +9 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +6 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/serialization.d.ts +11 -0
  125. package/dist/memory/serialization.d.ts.map +1 -0
  126. package/dist/memory/serialization.js +44 -0
  127. package/dist/memory/serialization.js.map +1 -0
  128. package/dist/migration/index.d.ts +5 -0
  129. package/dist/migration/index.d.ts.map +1 -0
  130. package/dist/migration/index.js +4 -0
  131. package/dist/migration/index.js.map +1 -0
  132. package/dist/migration/migrate.d.ts +11 -0
  133. package/dist/migration/migrate.d.ts.map +1 -0
  134. package/dist/migration/migrate.js +52 -0
  135. package/dist/migration/migrate.js.map +1 -0
  136. package/dist/migration/migrations.d.ts +22 -0
  137. package/dist/migration/migrations.d.ts.map +1 -0
  138. package/dist/migration/migrations.js +10 -0
  139. package/dist/migration/migrations.js.map +1 -0
  140. package/dist/migration/version.d.ts +9 -0
  141. package/dist/migration/version.d.ts.map +1 -0
  142. package/dist/migration/version.js +9 -0
  143. package/dist/migration/version.js.map +1 -0
  144. package/dist/model/LLMModelDefinition.d.ts +7 -0
  145. package/dist/model/LLMModelDefinition.d.ts.map +1 -0
  146. package/dist/model/LLMModelDefinition.js +11 -0
  147. package/dist/model/LLMModelDefinition.js.map +1 -0
  148. package/dist/model/Model.d.ts +21 -0
  149. package/dist/model/Model.d.ts.map +1 -0
  150. package/dist/model/Model.js +15 -0
  151. package/dist/model/Model.js.map +1 -0
  152. package/dist/model/ModelDefinition.d.ts +15 -0
  153. package/dist/model/ModelDefinition.d.ts.map +1 -0
  154. package/dist/model/ModelDefinition.js +2 -0
  155. package/dist/model/ModelDefinition.js.map +1 -0
  156. package/dist/model/ModelRegistry.d.ts +17 -0
  157. package/dist/model/ModelRegistry.d.ts.map +1 -0
  158. package/dist/model/ModelRegistry.js +27 -0
  159. package/dist/model/ModelRegistry.js.map +1 -0
  160. package/dist/model/index.d.ts +8 -0
  161. package/dist/model/index.d.ts.map +1 -0
  162. package/dist/model/index.js +5 -0
  163. package/dist/model/index.js.map +1 -0
  164. package/dist/model/serialization.d.ts +8 -0
  165. package/dist/model/serialization.d.ts.map +1 -0
  166. package/dist/model/serialization.js +25 -0
  167. package/dist/model/serialization.js.map +1 -0
  168. package/dist/node/AgentNode.d.ts +21 -0
  169. package/dist/node/AgentNode.d.ts.map +1 -0
  170. package/dist/node/AgentNode.js +60 -0
  171. package/dist/node/AgentNode.js.map +1 -0
  172. package/dist/node/DataNode.d.ts +14 -0
  173. package/dist/node/DataNode.d.ts.map +1 -0
  174. package/dist/node/DataNode.js +26 -0
  175. package/dist/node/DataNode.js.map +1 -0
  176. package/dist/node/FunctionNode.d.ts +24 -0
  177. package/dist/node/FunctionNode.d.ts.map +1 -0
  178. package/dist/node/FunctionNode.js +44 -0
  179. package/dist/node/FunctionNode.js.map +1 -0
  180. package/dist/node/InputNode.d.ts +46 -0
  181. package/dist/node/InputNode.d.ts.map +1 -0
  182. package/dist/node/InputNode.js +133 -0
  183. package/dist/node/InputNode.js.map +1 -0
  184. package/dist/node/LogicNode.d.ts +13 -0
  185. package/dist/node/LogicNode.d.ts.map +1 -0
  186. package/dist/node/LogicNode.js +19 -0
  187. package/dist/node/LogicNode.js.map +1 -0
  188. package/dist/node/MqttNode.d.ts +27 -0
  189. package/dist/node/MqttNode.d.ts.map +1 -0
  190. package/dist/node/MqttNode.js +96 -0
  191. package/dist/node/MqttNode.js.map +1 -0
  192. package/dist/node/Node.d.ts +49 -0
  193. package/dist/node/Node.d.ts.map +1 -0
  194. package/dist/node/Node.js +9 -0
  195. package/dist/node/Node.js.map +1 -0
  196. package/dist/node/NodeDefinition.d.ts +30 -0
  197. package/dist/node/NodeDefinition.d.ts.map +1 -0
  198. package/dist/node/NodeDefinition.js +2 -0
  199. package/dist/node/NodeDefinition.js.map +1 -0
  200. package/dist/node/NodeRegistry.d.ts +19 -0
  201. package/dist/node/NodeRegistry.d.ts.map +1 -0
  202. package/dist/node/NodeRegistry.js +65 -0
  203. package/dist/node/NodeRegistry.js.map +1 -0
  204. package/dist/node/OutputNode.d.ts +23 -0
  205. package/dist/node/OutputNode.d.ts.map +1 -0
  206. package/dist/node/OutputNode.js +62 -0
  207. package/dist/node/OutputNode.js.map +1 -0
  208. package/dist/node/ToolNode.d.ts +12 -0
  209. package/dist/node/ToolNode.d.ts.map +1 -0
  210. package/dist/node/ToolNode.js +18 -0
  211. package/dist/node/ToolNode.js.map +1 -0
  212. package/dist/node/TriggerNode.d.ts +67 -0
  213. package/dist/node/TriggerNode.d.ts.map +1 -0
  214. package/dist/node/TriggerNode.js +172 -0
  215. package/dist/node/TriggerNode.js.map +1 -0
  216. package/dist/node/constants.d.ts +16 -0
  217. package/dist/node/constants.d.ts.map +1 -0
  218. package/dist/node/constants.js +18 -0
  219. package/dist/node/constants.js.map +1 -0
  220. package/dist/node/index.d.ts +12 -0
  221. package/dist/node/index.d.ts.map +1 -0
  222. package/dist/node/index.js +9 -0
  223. package/dist/node/index.js.map +1 -0
  224. package/dist/node/methods.d.ts +73 -0
  225. package/dist/node/methods.d.ts.map +1 -0
  226. package/dist/node/methods.js +261 -0
  227. package/dist/node/methods.js.map +1 -0
  228. package/dist/node/serialization.d.ts +25 -0
  229. package/dist/node/serialization.d.ts.map +1 -0
  230. package/dist/node/serialization.js +525 -0
  231. package/dist/node/serialization.js.map +1 -0
  232. package/dist/parameter/OutputParameter.d.ts +69 -0
  233. package/dist/parameter/OutputParameter.d.ts.map +1 -0
  234. package/dist/parameter/OutputParameter.js +6 -0
  235. package/dist/parameter/OutputParameter.js.map +1 -0
  236. package/dist/parameter/Parameter.d.ts +152 -0
  237. package/dist/parameter/Parameter.d.ts.map +1 -0
  238. package/dist/parameter/Parameter.js +86 -0
  239. package/dist/parameter/Parameter.js.map +1 -0
  240. package/dist/parameter/index.d.ts +5 -0
  241. package/dist/parameter/index.d.ts.map +1 -0
  242. package/dist/parameter/index.js +3 -0
  243. package/dist/parameter/index.js.map +1 -0
  244. package/dist/variable/Variable.d.ts +25 -0
  245. package/dist/variable/Variable.d.ts.map +1 -0
  246. package/dist/variable/Variable.js +2 -0
  247. package/dist/variable/Variable.js.map +1 -0
  248. package/dist/variable/index.d.ts +3 -0
  249. package/dist/variable/index.d.ts.map +1 -0
  250. package/dist/variable/index.js +5 -0
  251. package/dist/variable/index.js.map +1 -0
  252. package/dist/variable/operations.d.ts +37 -0
  253. package/dist/variable/operations.d.ts.map +1 -0
  254. package/dist/variable/operations.js +88 -0
  255. package/dist/variable/operations.js.map +1 -0
  256. package/dist/workflow/Workflow.d.ts +38 -0
  257. package/dist/workflow/Workflow.d.ts.map +1 -0
  258. package/dist/workflow/Workflow.js +8 -0
  259. package/dist/workflow/Workflow.js.map +1 -0
  260. package/dist/workflow/index.d.ts +4 -0
  261. package/dist/workflow/index.d.ts.map +1 -0
  262. package/dist/workflow/index.js +3 -0
  263. package/dist/workflow/index.js.map +1 -0
  264. package/dist/workflow/serialization.d.ts +43 -0
  265. package/dist/workflow/serialization.d.ts.map +1 -0
  266. package/dist/workflow/serialization.js +215 -0
  267. package/dist/workflow/serialization.js.map +1 -0
  268. package/package.json +105 -0
  269. package/src/api/index.ts +11 -0
  270. package/src/api/workflow.ts +607 -0
  271. package/src/channel/Channel.ts +11 -0
  272. package/src/channel/ChannelDefinition.ts +76 -0
  273. package/src/channel/index.ts +6 -0
  274. package/src/channel/serialization.ts +68 -0
  275. package/src/deploy/index.ts +1 -0
  276. package/src/deploy/requirements.test.ts +61 -0
  277. package/src/deploy/requirements.ts +41 -0
  278. package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +158 -0
  279. package/src/diagnostics/diagnostics.test.ts +878 -0
  280. package/src/diagnostics/diagnostics.ts +936 -0
  281. package/src/diagnostics/index.ts +11 -0
  282. package/src/edge/Edge.ts +23 -0
  283. package/src/edge/EdgeDefinition.ts +45 -0
  284. package/src/edge/EdgeType.ts +19 -0
  285. package/src/edge/index.ts +8 -0
  286. package/src/edge/serialization.ts +83 -0
  287. package/src/expression/index.ts +4 -0
  288. package/src/expression/parser.ts +362 -0
  289. package/src/expression/types.ts +30 -0
  290. package/src/function/FunctionDeclaration.ts +54 -0
  291. package/src/function/index.ts +3 -0
  292. package/src/function/serialization.ts +40 -0
  293. package/src/globals.d.ts +9 -0
  294. package/src/id/index.ts +8 -0
  295. package/src/index.ts +22 -0
  296. package/src/memory/Memory.ts +15 -0
  297. package/src/memory/MemoryDefinition.ts +16 -0
  298. package/src/memory/MemoryFileDefinition.ts +37 -0
  299. package/src/memory/MemoryRegistry.ts +35 -0
  300. package/src/memory/VectorDatabaseDefinition.ts +21 -0
  301. package/src/memory/index.ts +8 -0
  302. package/src/memory/serialization.ts +47 -0
  303. package/src/migration/index.ts +4 -0
  304. package/src/migration/migrate.test.ts +44 -0
  305. package/src/migration/migrate.ts +58 -0
  306. package/src/migration/migrations.ts +24 -0
  307. package/src/migration/version.ts +9 -0
  308. package/src/model/LLMModelDefinition.ts +12 -0
  309. package/src/model/Model.ts +39 -0
  310. package/src/model/ModelDefinition.ts +15 -0
  311. package/src/model/ModelRegistry.ts +33 -0
  312. package/src/model/index.ts +7 -0
  313. package/src/model/serialization.ts +30 -0
  314. package/src/node/AgentNode.ts +82 -0
  315. package/src/node/DataNode.ts +41 -0
  316. package/src/node/FunctionNode.ts +76 -0
  317. package/src/node/InputNode.ts +185 -0
  318. package/src/node/LogicNode.ts +33 -0
  319. package/src/node/MqttNode.ts +127 -0
  320. package/src/node/Node.ts +61 -0
  321. package/src/node/NodeDefinition.ts +37 -0
  322. package/src/node/NodeRegistry.ts +85 -0
  323. package/src/node/OutputNode.ts +87 -0
  324. package/src/node/ToolNode.ts +32 -0
  325. package/src/node/TriggerNode.ts +272 -0
  326. package/src/node/constants.ts +16 -0
  327. package/src/node/index.ts +26 -0
  328. package/src/node/methods.ts +278 -0
  329. package/src/node/serialization.ts +544 -0
  330. package/src/parameter/OutputParameter.ts +68 -0
  331. package/src/parameter/Parameter.ts +243 -0
  332. package/src/parameter/index.ts +33 -0
  333. package/src/variable/Variable.ts +10 -0
  334. package/src/variable/index.ts +16 -0
  335. package/src/variable/operations.ts +106 -0
  336. package/src/workflow/Workflow.ts +41 -0
  337. package/src/workflow/index.ts +3 -0
  338. package/src/workflow/serialization.test.ts +240 -0
  339. package/src/workflow/serialization.ts +242 -0
@@ -0,0 +1,936 @@
1
+ import type { NodeDefinition, NodeData } from "../node";
2
+ import type { Expression } from "../api";
3
+ import { NodeCategory, NodeRegistry } from "../node";
4
+ import type { EdgeData, EdgeType } from "../edge";
5
+ import { getEdgeDefinition, isControlFlow } from "../edge";
6
+ import { getArguments, getNodeAvailableOutput, getOutputBinding, getPorts, isNodeUsedAsTool } from "../node/methods";
7
+ import type { Edge } from "../edge";
8
+ import type { Variable } from "../variable";
9
+ import { computeAvailableVariables, refToLookupKey } from "../variable";
10
+ import { isExpression, resolveExpression } from "../expression/types";
11
+ import { parseExpression } from "../expression/parser";
12
+ import { isParameterActive, isEmpty, resolveExpressionType, resolveChannelTypes, resolveMemoryTypes, resolveModelTypes } from "../parameter";
13
+ import type { ExpressionParam, ChannelSelectParam, MemorySelectParam, ModelSelectParam, OutputDeclaration } from "../parameter";
14
+ import type { Channel } from "../channel";
15
+ import { CHANNEL_DEFINITION } from "../channel";
16
+ import type { Memory } from "../memory";
17
+ import { MemoryRegistry } from "../memory";
18
+ import type { Model } from "../model";
19
+ import { ModelRegistry } from "../model";
20
+ import type { Schemas } from "../api";
21
+ import type { Reference } from "../api";
22
+ import { FunctionCallNode, buildFunctionNodeDef } from "../node/FunctionNode";
23
+ import type { FunctionDeclaration, FunctionInfo, OutputAssignment } from "../function";
24
+ import { toFunctionInfo } from "../function";
25
+ import { MAIN_CANVAS_ID, type Workflow, type Canvas } from "../workflow/Workflow";
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ export type DiagnosticSeverity = "error" | "warning";
32
+
33
+ export type DiagnosticCategory =
34
+ | "missing-required-param"
35
+ | "invalid-expression"
36
+ | "invalid-reference"
37
+ | "function-deleted"
38
+ | "function-stale"
39
+ | "unconnected-input"
40
+ | "unconnected-output"
41
+ | "tool-not-connected"
42
+ | "missing-output-assignment"
43
+ | "assign-type-mismatch"
44
+ | "duplicate-output-name";
45
+
46
+ export interface Diagnostic {
47
+ severity: DiagnosticSeverity;
48
+ category: DiagnosticCategory;
49
+ message: string;
50
+ /** Canvas this diagnostic belongs to. Omitted for project-scoped sources (e.g. channels). */
51
+ canvasId?: string;
52
+ nodeId?: string;
53
+ edgeId?: string;
54
+ /** Set when the diagnostic targets a project-scoped channel. */
55
+ channelId?: string;
56
+ /** Set when the diagnostic targets a project-scoped memory primitive. */
57
+ memoryId?: string;
58
+ /** Set when the diagnostic targets a project-scoped declared model. */
59
+ modelId?: string;
60
+ /** Set when the diagnostic targets a project-scoped function declaration. */
61
+ functionId?: string;
62
+ paramId?: string;
63
+ outputId?: string; // For output binding diagnostics
64
+ }
65
+
66
+ /**
67
+ * Compute diagnostics for a single node.
68
+ */
69
+ export function computeNodeDiagnostics(opts: {
70
+ canvasId: string;
71
+ nodeId: string;
72
+ nodeData: NodeData;
73
+ nodeDefinition: NodeDefinition | undefined;
74
+ availableVariables: Record<string, Variable>;
75
+ channels: Record<string, Channel>;
76
+ memory?: Record<string, Memory>;
77
+ /** Declared custom models (project-scoped). */
78
+ models?: Record<string, Model>;
79
+ /** Ids in the static model catalog (props-supplied). Undefined headlessly — catalog ids then aren't flagged. */
80
+ availableModelIds?: Set<string>;
81
+ edges: readonly Edge[];
82
+ isStale?: boolean;
83
+ isDeleted?: boolean;
84
+ }): Diagnostic[] {
85
+ const {
86
+ canvasId,
87
+ nodeId,
88
+ nodeData,
89
+ nodeDefinition,
90
+ availableVariables,
91
+ channels,
92
+ memory,
93
+ models,
94
+ availableModelIds,
95
+ edges,
96
+ isStale = false,
97
+ isDeleted = false,
98
+ } = opts;
99
+
100
+ const diags: Diagnostic[] = [];
101
+
102
+ // --- Function-specific diagnostics ---
103
+ if (isDeleted) {
104
+ diags.push({
105
+ severity: "error",
106
+ category: "function-deleted",
107
+ canvasId,
108
+ nodeId,
109
+ message: "Function has been deleted. Remove this node.",
110
+ });
111
+ } else if (isStale) {
112
+ diags.push({
113
+ severity: "warning",
114
+ category: "function-stale",
115
+ canvasId,
116
+ nodeId,
117
+ message: "Function definition has changed. Please update this node.",
118
+ });
119
+ }
120
+
121
+ if (!nodeDefinition) return diags;
122
+
123
+ const portDefinitions = getPorts(nodeData);
124
+ const parameters = getArguments(nodeData);
125
+ const usedAsToolInput = isNodeUsedAsTool(nodeId, nodeData, edges);
126
+
127
+ // --- Parameter diagnostics ---
128
+ for (const param of nodeDefinition.parameters) {
129
+ if (!isParameterActive(param, parameters, usedAsToolInput)) continue;
130
+ const value = parameters[param.id];
131
+
132
+ // invalid-expression (skip empty — caught by missing-required-param)
133
+ if (isExpression(value) && value.expression) {
134
+ const expr = resolveExpression(value, availableVariables);
135
+ // Resolve expressionType (static, args-only lambda, or derived from a referenced variable)
136
+ if (param.type === "expression") {
137
+ expr.expectedType = resolveExpressionType(param as ExpressionParam, parameters, availableVariables);
138
+ }
139
+ const parseRes = parseExpression(expr);
140
+ if (!parseRes.isValid) {
141
+ diags.push({
142
+ severity: "error",
143
+ category: "invalid-expression",
144
+ canvasId,
145
+ nodeId,
146
+ paramId: param.id,
147
+ message: `Invalid expression for "${param.label}": ${parseRes.errors.join(", ")}`,
148
+ });
149
+ }
150
+ }
151
+
152
+ // missing-required-param
153
+ if (!param.optional) {
154
+ const isEmptyExpression = isExpression(value) && !value.expression;
155
+ const isEmptyReference =
156
+ param.type === "variableSelect" &&
157
+ (!value || (typeof value === "object" && value !== null && !(value as { varId?: string }).varId));
158
+ if (isEmpty(value) || isEmptyExpression || isEmptyReference) {
159
+ diags.push({
160
+ severity: "error",
161
+ category: "missing-required-param",
162
+ canvasId,
163
+ nodeId,
164
+ paramId: param.id,
165
+ message: `Missing required parameter "${param.label}"`,
166
+ });
167
+ }
168
+ }
169
+
170
+ // invalid-reference: variableSelect points to deleted variable
171
+ if (param.type === "variableSelect" && value) {
172
+ const ref = value as Reference;
173
+ if (ref.varId) {
174
+ const key = refToLookupKey(ref);
175
+ if (!availableVariables[key]) {
176
+ diags.push({
177
+ severity: "error",
178
+ category: "invalid-reference",
179
+ canvasId,
180
+ nodeId,
181
+ paramId: param.id,
182
+ message: `"${param.label}" references a deleted variable`,
183
+ });
184
+ }
185
+ }
186
+ }
187
+
188
+ // invalid-reference: channelSelect points to deleted or incompatible channel
189
+ if (param.type === "channelSelect" && value) {
190
+ const channelId = value as string;
191
+ const channel = Object.values(channels).find((v) => v.id === channelId);
192
+ if (!channel) {
193
+ diags.push({
194
+ severity: "error",
195
+ category: "invalid-reference",
196
+ canvasId,
197
+ nodeId,
198
+ paramId: param.id,
199
+ message: `"${param.label}" references a deleted channel`,
200
+ });
201
+ } else {
202
+ const channelParam = param as ChannelSelectParam;
203
+ const allowedTypes = resolveChannelTypes(channelParam, parameters);
204
+ if (!allowedTypes.includes(channel.type)) {
205
+ diags.push({
206
+ severity: "error",
207
+ category: "invalid-reference",
208
+ canvasId,
209
+ nodeId,
210
+ paramId: param.id,
211
+ message: `"${param.label}" references "${channel.label}" (${channel.type}), which is not a compatible channel type`,
212
+ });
213
+ }
214
+ }
215
+ }
216
+
217
+ // invalid-reference: memory-refs entries point to deleted memory files
218
+ if (param.type === "memory-refs" && Array.isArray(value) && memory) {
219
+ const refs = value as Schemas["MemoryRef"][];
220
+ const knownIds = new Set(
221
+ Object.values(memory)
222
+ .filter((m) => m.type === "MemoryFile")
223
+ .map((m) => m.id),
224
+ );
225
+ const missing = refs.filter((r) => !r.id || !knownIds.has(r.id));
226
+ if (missing.length > 0) {
227
+ diags.push({
228
+ severity: "error",
229
+ category: "invalid-reference",
230
+ canvasId,
231
+ nodeId,
232
+ paramId: param.id,
233
+ message: `"${param.label}" references ${missing.length} deleted memory file${missing.length === 1 ? "" : "s"}`,
234
+ });
235
+ }
236
+ }
237
+
238
+ // invalid-reference: memorySelect points to deleted or incompatible memory
239
+ if (param.type === "memorySelect" && value && memory) {
240
+ const memoryId = value as string;
241
+ const mem = Object.values(memory).find((m) => m.id === memoryId);
242
+ if (!mem) {
243
+ diags.push({
244
+ severity: "error",
245
+ category: "invalid-reference",
246
+ canvasId,
247
+ nodeId,
248
+ paramId: param.id,
249
+ message: `"${param.label}" references a deleted memory`,
250
+ });
251
+ } else {
252
+ const memoryParam = param as MemorySelectParam;
253
+ const allowedTypes = resolveMemoryTypes(memoryParam, parameters);
254
+ if (!allowedTypes.includes(mem.type)) {
255
+ diags.push({
256
+ severity: "error",
257
+ category: "invalid-reference",
258
+ canvasId,
259
+ nodeId,
260
+ paramId: param.id,
261
+ message: `"${param.label}" references "${mem.label}" (${mem.type}), which is not a compatible memory type`,
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ // invalid-reference: modelSelect points to a deleted custom model or unknown catalog id
268
+ if (param.type === "modelSelect" && value && models) {
269
+ const modelId = value as string;
270
+ const custom = Object.values(models).find((m) => m.id === modelId);
271
+ if (custom) {
272
+ const modelParam = param as ModelSelectParam;
273
+ const allowedTypes = resolveModelTypes(modelParam, parameters);
274
+ if (!allowedTypes.includes(custom.type)) {
275
+ diags.push({
276
+ severity: "error",
277
+ category: "invalid-reference",
278
+ canvasId,
279
+ nodeId,
280
+ paramId: param.id,
281
+ message: `"${param.label}" references "${custom.label}" (${custom.type}), which is not a compatible model type`,
282
+ });
283
+ }
284
+ } else if (availableModelIds && !availableModelIds.has(modelId)) {
285
+ // Not a declared custom model and not in the supplied catalog → stale.
286
+ // Headlessly (no catalog) static ids can't be verified, so we don't flag.
287
+ diags.push({
288
+ severity: "error",
289
+ category: "invalid-reference",
290
+ canvasId,
291
+ nodeId,
292
+ paramId: param.id,
293
+ message: `"${param.label}" references a deleted model`,
294
+ });
295
+ }
296
+ }
297
+ }
298
+
299
+ // --- Output binding diagnostics (skip when node is used as tool — outputs are scoped out) ---
300
+ if (!usedAsToolInput) {
301
+ const availableOutput = getNodeAvailableOutput(nodeData);
302
+ for (const outputId of Object.keys(availableOutput)) {
303
+ const binding = getOutputBinding(nodeData, outputId);
304
+ // Inactive bindings are discarded — nothing to validate.
305
+ if (!binding || !binding.active || binding.mode !== "assign") continue;
306
+
307
+ const outputDef = availableOutput[outputId];
308
+ if (!binding.target.srcId) {
309
+ diags.push({
310
+ severity: "error",
311
+ category: "assign-type-mismatch",
312
+ canvasId,
313
+ nodeId,
314
+ outputId,
315
+ message: `Output "${outputDef?.name ?? outputId}" has no variable selected`,
316
+ });
317
+ continue;
318
+ }
319
+ const key = refToLookupKey(binding.target);
320
+ const targetVar = availableVariables[key];
321
+ if (!targetVar) {
322
+ diags.push({
323
+ severity: "error",
324
+ category: "assign-type-mismatch",
325
+ canvasId,
326
+ nodeId,
327
+ outputId,
328
+ message: `Output "${outputDef?.name ?? outputId}" assigns to a deleted variable`,
329
+ });
330
+ } else if (outputDef && targetVar.dataType !== outputDef.dataType) {
331
+ diags.push({
332
+ severity: "error",
333
+ category: "assign-type-mismatch",
334
+ canvasId,
335
+ nodeId,
336
+ outputId,
337
+ message: `Output "${outputDef.name}" (${outputDef.dataType}) cannot assign to "${targetVar.name}" (${targetVar.dataType})`,
338
+ });
339
+ }
340
+ }
341
+
342
+ // --- List-output declaration diagnostics ---
343
+ // Walk each list-output entry. Two layers of validation:
344
+ // 1. Per-entry: name non-empty, assign target valid + type-compatible.
345
+ // 2. Per-list: names unique within the list — the `name` field doubles as the
346
+ // JSON property name in the LLM's structured response, so duplicates would
347
+ // silently collide. Required for both modes (emit and assign).
348
+ const listOutputs = (nodeDefinition.outputs ?? []).filter((o) => o.type === "list");
349
+ for (const out of listOutputs) {
350
+ const entries = ((nodeData.arguments as Record<string, unknown>)[out.id] as OutputDeclaration[] | undefined) ?? [];
351
+
352
+ // Build a name → indices map up-front; flag duplicates and empties on a per-entry basis.
353
+ const nameToIndices = new Map<string, number[]>();
354
+ entries.forEach((entry, index) => {
355
+ const arr = nameToIndices.get(entry.name);
356
+ if (arr) arr.push(index);
357
+ else nameToIndices.set(entry.name, [index]);
358
+ });
359
+
360
+ entries.forEach((entry, index) => {
361
+ const outputId = `${out.id}[${index}]`;
362
+
363
+ // missing name (both modes)
364
+ if (!entry.name || entry.name.trim() === "") {
365
+ diags.push({
366
+ severity: "error",
367
+ category: "missing-required-param",
368
+ canvasId,
369
+ nodeId,
370
+ outputId,
371
+ message: `${out.label} entry #${index + 1} has no name`,
372
+ });
373
+ } else {
374
+ const collisions = nameToIndices.get(entry.name) ?? [];
375
+ if (collisions.length > 1) {
376
+ diags.push({
377
+ severity: "error",
378
+ category: "duplicate-output-name",
379
+ canvasId,
380
+ nodeId,
381
+ outputId,
382
+ message: `${out.label} entry #${index + 1} has duplicate name "${entry.name}"`,
383
+ });
384
+ }
385
+ }
386
+
387
+ if (entry.mode !== "assign") return;
388
+ // Use a synthetic outputId: `<listId>[<index>]`. Stable per-position, surfaces the
389
+ // error on the node badge. (ListOutputSection could light up individual rows from
390
+ // this key in a follow-up.)
391
+ if (!entry.target.srcId) {
392
+ diags.push({
393
+ severity: "error",
394
+ category: "assign-type-mismatch",
395
+ canvasId,
396
+ nodeId,
397
+ outputId,
398
+ message: `${out.label} entry #${index + 1} has no variable selected`,
399
+ });
400
+ return;
401
+ }
402
+ const key = refToLookupKey(entry.target);
403
+ const targetVar = availableVariables[key];
404
+ if (!targetVar) {
405
+ diags.push({
406
+ severity: "error",
407
+ category: "assign-type-mismatch",
408
+ canvasId,
409
+ nodeId,
410
+ outputId,
411
+ message: `${out.label} entry #${index + 1} assigns to a deleted variable`,
412
+ });
413
+ } else if (targetVar.dataType !== entry.dataType) {
414
+ diags.push({
415
+ severity: "error",
416
+ category: "assign-type-mismatch",
417
+ canvasId,
418
+ nodeId,
419
+ outputId,
420
+ message: `${out.label} entry #${index + 1} (${entry.dataType}) cannot assign to "${targetVar.name}" (${targetVar.dataType})`,
421
+ });
422
+ }
423
+ });
424
+ }
425
+ }
426
+
427
+ // --- Port connectivity diagnostics ---
428
+ const controlInputs = portDefinitions.input.filter((p) => p.type === "control");
429
+ const toolInputPorts = portDefinitions.input.filter((p) => p.type === "tool");
430
+ const controlOutputs = portDefinitions.output.filter((p) => p.type === "control");
431
+ const connectedTargetHandles = new Set(edges.filter((e) => e.target === nodeId).map((e) => e.targetHandle));
432
+ const connectedSourceHandles = new Set(edges.filter((e) => e.source === nodeId).map((e) => e.sourceHandle));
433
+
434
+ // Check unconnected control inputs (applies to ALL nodes that have them)
435
+ if (controlInputs.length > 0) {
436
+ if (usedAsToolInput) {
437
+ for (const port of toolInputPorts) {
438
+ if (!connectedTargetHandles.has(port.id)) {
439
+ diags.push({
440
+ severity: "warning",
441
+ category: "unconnected-input",
442
+ canvasId,
443
+ nodeId,
444
+ message: `Node is not connected and will never run`,
445
+ });
446
+ }
447
+ }
448
+ } else {
449
+ for (const port of controlInputs) {
450
+ if (!connectedTargetHandles.has(port.id)) {
451
+ diags.push({
452
+ severity: "warning",
453
+ category: "unconnected-input",
454
+ canvasId,
455
+ nodeId,
456
+ message: `Node is not connected and will never run`,
457
+ });
458
+ }
459
+ }
460
+ }
461
+ } else if (toolInputPorts.length > 0) {
462
+ // Tool-only input nodes (e.g. WebSearchTool)
463
+ const hasAnyConnection = toolInputPorts.some((p) => connectedTargetHandles.has(p.id));
464
+ if (!hasAnyConnection) {
465
+ diags.push({
466
+ severity: "warning",
467
+ category: "tool-not-connected",
468
+ canvasId,
469
+ nodeId,
470
+ message: `"${nodeDefinition.label}" is not connected to an agent`,
471
+ });
472
+ }
473
+ }
474
+
475
+ // Check unconnected control outputs (triggers only — they are entry points that must lead somewhere)
476
+ if (controlOutputs.length > 0 && nodeDefinition.category === NodeCategory.Trigger) {
477
+ for (const port of controlOutputs) {
478
+ if (!connectedSourceHandles.has(port.id)) {
479
+ diags.push({
480
+ severity: "warning",
481
+ category: "unconnected-output",
482
+ canvasId,
483
+ nodeId,
484
+ message: `"${nodeDefinition.label}" has no outgoing connection — nothing will run after it`,
485
+ });
486
+ }
487
+ }
488
+ }
489
+
490
+ return diags;
491
+ }
492
+
493
+ /**
494
+ * Compute diagnostics for a single edge. Extracted from CustomEdge's useMemo.
495
+ */
496
+ export function computeEdgeDiagnostics(opts: {
497
+ canvasId: string;
498
+ edgeId: string;
499
+ edgeType: EdgeType;
500
+ edgeData: EdgeData | undefined;
501
+ availableVariables: Record<string, Variable>;
502
+ sourceControlEdgeCount: number;
503
+ }): Diagnostic[] {
504
+ const { canvasId, edgeId, edgeType, edgeData, availableVariables, sourceControlEdgeCount } = opts;
505
+ const diags: Diagnostic[] = [];
506
+ const def = getEdgeDefinition(edgeType);
507
+ if (def.parameters.length === 0) return diags;
508
+
509
+ const isBranching = sourceControlEdgeCount > 1;
510
+
511
+ for (const param of def.parameters) {
512
+ // Description is optional on agent output edges when not branching
513
+ if (param.id === "description" && !isBranching && (edgeType === "agentChoice" || edgeType === "agentDelegate")) {
514
+ continue;
515
+ }
516
+
517
+ const value = edgeData?.[param.id];
518
+
519
+ if (param.type === "expression") {
520
+ const exprValue = value as Expression | undefined;
521
+ if (!exprValue?.expression) {
522
+ diags.push({
523
+ severity: "error",
524
+ category: "missing-required-param",
525
+ canvasId,
526
+ edgeId,
527
+ paramId: param.id,
528
+ message: `Missing required parameter "${param.label}" on edge`,
529
+ });
530
+ continue;
531
+ }
532
+ if (isExpression(exprValue)) {
533
+ const expr = resolveExpression(exprValue, availableVariables);
534
+ const parseRes = parseExpression(expr);
535
+ if (!parseRes.isValid) {
536
+ diags.push({
537
+ severity: "error",
538
+ category: "invalid-expression",
539
+ canvasId,
540
+ edgeId,
541
+ paramId: param.id,
542
+ message: `Invalid expression for "${param.label}": ${parseRes.errors.join(", ")}`,
543
+ });
544
+ }
545
+ }
546
+ } else {
547
+ if (!value) {
548
+ diags.push({
549
+ severity: "error",
550
+ category: "missing-required-param",
551
+ canvasId,
552
+ edgeId,
553
+ paramId: param.id,
554
+ message: `Missing required parameter "${param.label}" on edge`,
555
+ });
556
+ }
557
+ }
558
+ }
559
+
560
+ return diags;
561
+ }
562
+
563
+ /**
564
+ * Compute diagnostics for a single channel. Mirrors the parameter loop
565
+ * from computeNodeDiagnostics: filter to active params (per the type
566
+ * discriminator), then required-check each. Empty label is also flagged so
567
+ * the user has a non-blank name in `channelSelect` dropdowns.
568
+ */
569
+ export function validateChannel(channel: Channel): Diagnostic[] {
570
+ const diags: Diagnostic[] = [];
571
+
572
+ if (!channel.label || channel.label.trim() === "") {
573
+ diags.push({
574
+ severity: "error",
575
+ category: "missing-required-param",
576
+ channelId: channel.id,
577
+ message: `Channel has no label`,
578
+ });
579
+ }
580
+
581
+ // `type` is mirrored into the args record so activation rules can read it.
582
+ const args: Record<string, unknown> = { ...channel.arguments, type: channel.type };
583
+ for (const param of CHANNEL_DEFINITION.parameters) {
584
+ if (param.id === "type") continue; // top-level discriminator, always set
585
+ if (!isParameterActive(param, args, false)) continue;
586
+ if (param.optional) continue;
587
+
588
+ const value = channel.arguments[param.id];
589
+ if (isEmpty(value)) {
590
+ diags.push({
591
+ severity: "error",
592
+ category: "missing-required-param",
593
+ channelId: channel.id,
594
+ paramId: param.id,
595
+ message: `Missing required parameter "${param.label}" on channel "${channel.label}"`,
596
+ });
597
+ }
598
+ }
599
+
600
+ return diags;
601
+ }
602
+
603
+ /**
604
+ * Compute diagnostics for a single memory primitive. Mirrors validateChannel:
605
+ * an empty label is flagged (so memorySelect/memory-refs dropdowns have a
606
+ * non-blank name), then each required parameter for the memory's type is checked.
607
+ */
608
+ export function validateMemory(mem: Memory): Diagnostic[] {
609
+ const diags: Diagnostic[] = [];
610
+
611
+ if (!mem.label || mem.label.trim() === "") {
612
+ diags.push({
613
+ severity: "error",
614
+ category: "missing-required-param",
615
+ memoryId: mem.id,
616
+ message: `Memory has no label`,
617
+ });
618
+ }
619
+
620
+ const def = MemoryRegistry.getByType(mem.type);
621
+ for (const param of def?.parameters ?? []) {
622
+ if (!isParameterActive(param, mem.arguments, false)) continue;
623
+ if (param.optional) continue;
624
+
625
+ const value = mem.arguments[param.id];
626
+ if (isEmpty(value)) {
627
+ diags.push({
628
+ severity: "error",
629
+ category: "missing-required-param",
630
+ memoryId: mem.id,
631
+ paramId: param.id,
632
+ message: `Missing required parameter "${param.label}" on memory "${mem.label}"`,
633
+ });
634
+ }
635
+ }
636
+
637
+ return diags;
638
+ }
639
+
640
+ /**
641
+ * Compute diagnostics for a single declared (custom) model. Mirrors
642
+ * validateMemory: an empty label is flagged, then each required parameter for
643
+ * the model's type is checked (LLMModel has none today, so this is label-only).
644
+ */
645
+ export function validateModel(model: Model): Diagnostic[] {
646
+ const diags: Diagnostic[] = [];
647
+
648
+ if (!model.label || model.label.trim() === "") {
649
+ diags.push({
650
+ severity: "error",
651
+ category: "missing-required-param",
652
+ modelId: model.id,
653
+ message: `Model has no label`,
654
+ });
655
+ }
656
+
657
+ const def = ModelRegistry.getByType(model.type);
658
+ for (const param of def?.parameters ?? []) {
659
+ if (!isParameterActive(param, model.arguments, false)) continue;
660
+ if (param.optional) continue;
661
+
662
+ const value = model.arguments[param.id];
663
+ if (isEmpty(value)) {
664
+ diags.push({
665
+ severity: "error",
666
+ category: "missing-required-param",
667
+ modelId: model.id,
668
+ paramId: param.id,
669
+ message: `Missing required parameter "${param.label}" on model "${model.label}"`,
670
+ });
671
+ }
672
+ }
673
+
674
+ return diags;
675
+ }
676
+
677
+ /**
678
+ * Validate a function's bundled output assignments, keyed by `outputId` so a caller
679
+ * can ring the specific row. The single home for this logic — shared by the live
680
+ * config panel, the project-scoped {@link validateFunction}, and the full
681
+ * {@link validateWorkflowState}.
682
+ *
683
+ * Without `availableVariables` (no body scope) it can only flag a *missing*
684
+ * expression (the engine-invariant check). Given the body's variables it also
685
+ * resolves + parses each expression, catching invalid references and type mismatches
686
+ * — the same `resolveExpression`/`parseExpression` path node expression params use.
687
+ */
688
+ export function validateFunctionOutputs(
689
+ outputs: readonly OutputAssignment[],
690
+ availableVariables?: Record<string, Variable>,
691
+ ): Diagnostic[] {
692
+ const diags: Diagnostic[] = [];
693
+ for (const out of outputs) {
694
+ const text = out.expression?.expression;
695
+ if (!text || text.trim() === "") {
696
+ diags.push({
697
+ severity: "error",
698
+ category: "missing-output-assignment",
699
+ outputId: out.uid,
700
+ message: `Missing return value assignment for "${out.name}"`,
701
+ });
702
+ } else if (availableVariables) {
703
+ const parseRes = parseExpression(resolveExpression(out.expression, availableVariables));
704
+ if (!parseRes.isValid) {
705
+ diags.push({
706
+ severity: "error",
707
+ category: "invalid-expression",
708
+ outputId: out.uid,
709
+ message: `Invalid expression for "${out.name}": ${parseRes.errors.join(", ")}`,
710
+ });
711
+ }
712
+ }
713
+ }
714
+ return diags;
715
+ }
716
+
717
+ /**
718
+ * Compute diagnostics for a single function declaration (the project-scoped
719
+ * signature + bundled output assignments, independent of the body canvas). Flags an
720
+ * empty name and any output without an assigned expression. Expression *validity*
721
+ * (which needs the body's variable scope) is left to the scoped callers — the config
722
+ * panel and validateWorkflowState (keyed by the function's canvas).
723
+ */
724
+ export function validateFunction(fn: FunctionDeclaration, availableVariables?: Record<string, Variable>): Diagnostic[] {
725
+ const diags: Diagnostic[] = [];
726
+
727
+ if (!fn.name || fn.name.trim() === "") {
728
+ diags.push({
729
+ severity: "error",
730
+ category: "missing-required-param",
731
+ functionId: fn.id,
732
+ message: `Function has no name`,
733
+ });
734
+ }
735
+
736
+ // Pass the body scope through when the caller has it (the sidebar sync supplies the
737
+ // function's canvas variables) so invalid/typed expressions surface too, not just
738
+ // missing ones. Tag with functionId for the sidebar list ring / tab badge.
739
+ for (const d of validateFunctionOutputs(fn.outputs, availableVariables)) diags.push({ ...d, functionId: fn.id });
740
+
741
+ return diags;
742
+ }
743
+
744
+ // ============================================================================
745
+ // Full-Project Validation Result Types
746
+ // ============================================================================
747
+
748
+ export interface CanvasValidationResult {
749
+ canvasId: string;
750
+ canvasLabel: string;
751
+ diagnostics: Diagnostic[];
752
+ errorCount: number;
753
+ warningCount: number;
754
+ }
755
+
756
+ export interface ValidationResult {
757
+ canvases: CanvasValidationResult[];
758
+ /** Project-scoped channel diagnostics (no canvasId). */
759
+ channelDiagnostics: Diagnostic[];
760
+ /** Project-scoped memory diagnostics (no canvasId). */
761
+ memoryDiagnostics: Diagnostic[];
762
+ /** Project-scoped declared-model diagnostics (no canvasId). */
763
+ modelDiagnostics: Diagnostic[];
764
+ totalErrors: number;
765
+ totalWarnings: number;
766
+ }
767
+
768
+ /**
769
+ * Derive the function registry from a snapshot's canvases. Mirrors
770
+ * `computeAllFunctions()` in useFunctionRegistry exactly: every non-main
771
+ * canvas that carries a `functionInfo` is a function, keyed by canvas id
772
+ * (which equals the function id by invariant).
773
+ *
774
+ * Deriving from the snapshot rather than the module-level cache makes
775
+ * validation deterministic from its input alone — and removes the cache-lag
776
+ * window the store-bound path had.
777
+ */
778
+ function deriveFunctionRegistry(functions: Record<string, FunctionDeclaration>): Record<string, FunctionInfo> {
779
+ const out: Record<string, FunctionInfo> = {};
780
+ for (const [id, decl] of Object.entries(functions)) out[id] = toFunctionInfo(decl);
781
+ return out;
782
+ }
783
+
784
+ /**
785
+ * Headless full-project validation. Pure: depends only on the passed
786
+ * {@link Workflow} (the in-memory domain shape) — no Zustand stores, no
787
+ * React, no DOM. Runnable in Node, a CLI, or a Claude Code skill.
788
+ *
789
+ * Two producers feed this: the editor reads its live stores into a
790
+ * `Workflow` literal; the CLI calls `deserialize(apiWorkflow)` from
791
+ * `../workflow/serialization` to convert the api JSON into this shape.
792
+ */
793
+ export function validateWorkflowState(state: Workflow): ValidationResult {
794
+ const canvasData = state.canvases ?? {};
795
+ const functionDecls = state.functions ?? {};
796
+ const allFunctions = deriveFunctionRegistry(functionDecls);
797
+ const channels = state.channels ?? {};
798
+ const memory = state.memory ?? {};
799
+ const models = state.models ?? {};
800
+
801
+ const canvases: CanvasValidationResult[] = [];
802
+ let totalErrors = 0;
803
+ let totalWarnings = 0;
804
+
805
+ for (const [canvasId, canvas] of Object.entries(canvasData)) {
806
+ const { nodes, edges } = canvas;
807
+
808
+ // Each canvas is fully self-contained — function canvases do not see main-canvas variables.
809
+ const { lookup: availableVariables } = computeAvailableVariables(canvas.variables, edges);
810
+
811
+ const canvasDiags: Diagnostic[] = [];
812
+
813
+ // Compute node diagnostics
814
+ for (const node of nodes) {
815
+ const nodeData = node;
816
+
817
+ // Resolve node definition
818
+ let nodeDefinition: NodeDefinition | undefined;
819
+ let isStale = false;
820
+ let isDeleted = false;
821
+
822
+ if (nodeData.type === "FunctionCall") {
823
+ const fnNode = nodeData as FunctionCallNode;
824
+ const registryFn = allFunctions[fnNode.functionInfo.id];
825
+ isDeleted = !registryFn;
826
+ isStale = registryFn ? fnNode.functionInfo.version !== registryFn.version : false;
827
+ if (registryFn) {
828
+ nodeDefinition = buildFunctionNodeDef(registryFn) as NodeDefinition;
829
+ } else {
830
+ nodeDefinition = buildFunctionNodeDef(fnNode.functionInfo) as NodeDefinition;
831
+ }
832
+ } else {
833
+ nodeDefinition = NodeRegistry.getByType(nodeData.type);
834
+ }
835
+
836
+ const diags = computeNodeDiagnostics({
837
+ canvasId,
838
+ nodeId: node.id,
839
+ nodeData,
840
+ nodeDefinition,
841
+ availableVariables,
842
+ channels,
843
+ memory,
844
+ models,
845
+ edges,
846
+ isStale,
847
+ isDeleted,
848
+ });
849
+
850
+ canvasDiags.push(...diags);
851
+ }
852
+
853
+ // Compute edge diagnostics
854
+ const sourceControlCounts = new Map<string, number>();
855
+ for (const edge of edges) {
856
+ if (isControlFlow(edge.type as EdgeType)) {
857
+ sourceControlCounts.set(edge.source, (sourceControlCounts.get(edge.source) ?? 0) + 1);
858
+ }
859
+ }
860
+
861
+ for (const edge of edges) {
862
+ const edgeType = (edge.type ?? "control") as EdgeType;
863
+ const diags = computeEdgeDiagnostics({
864
+ canvasId,
865
+ edgeId: edge.id,
866
+ edgeType,
867
+ edgeData: edge.data,
868
+ availableVariables,
869
+ sourceControlEdgeCount: sourceControlCounts.get(edge.source) ?? 0,
870
+ });
871
+
872
+ canvasDiags.push(...diags);
873
+ }
874
+
875
+ // Output assignment diagnostics: the function's declaration (functions[canvasId])
876
+ // owns the bundled outputs; their expressions resolve against this body's scope.
877
+ const fnDecl = functionDecls[canvasId];
878
+ if (fnDecl) {
879
+ for (const d of validateFunctionOutputs(fnDecl.outputs, availableVariables)) {
880
+ canvasDiags.push({ ...d, canvasId });
881
+ }
882
+ }
883
+
884
+ // Only include canvases with issues
885
+ if (canvasDiags.length > 0) {
886
+ const errorCount = canvasDiags.filter((d) => d.severity === "error").length;
887
+ const warningCount = canvasDiags.filter((d) => d.severity === "warning").length;
888
+
889
+ // Derive canvas label
890
+ const canvasLabel = canvasId === MAIN_CANVAS_ID ? "Main" : (functionDecls[canvasId]?.name ?? canvasId);
891
+
892
+ canvases.push({
893
+ canvasId,
894
+ canvasLabel,
895
+ diagnostics: canvasDiags,
896
+ errorCount,
897
+ warningCount,
898
+ });
899
+
900
+ totalErrors += errorCount;
901
+ totalWarnings += warningCount;
902
+ }
903
+ }
904
+
905
+ // Project-scoped channel diagnostics — independent of canvas iteration.
906
+ const channelDiagnostics: Diagnostic[] = [];
907
+ for (const channel of Object.values(channels)) {
908
+ channelDiagnostics.push(...validateChannel(channel));
909
+ }
910
+ for (const d of channelDiagnostics) {
911
+ if (d.severity === "error") totalErrors++;
912
+ else totalWarnings++;
913
+ }
914
+
915
+ // Project-scoped memory diagnostics — independent of canvas iteration.
916
+ const memoryDiagnostics: Diagnostic[] = [];
917
+ for (const mem of Object.values(memory)) {
918
+ memoryDiagnostics.push(...validateMemory(mem));
919
+ }
920
+ for (const d of memoryDiagnostics) {
921
+ if (d.severity === "error") totalErrors++;
922
+ else totalWarnings++;
923
+ }
924
+
925
+ // Project-scoped declared-model diagnostics — independent of canvas iteration.
926
+ const modelDiagnostics: Diagnostic[] = [];
927
+ for (const model of Object.values(models)) {
928
+ modelDiagnostics.push(...validateModel(model));
929
+ }
930
+ for (const d of modelDiagnostics) {
931
+ if (d.severity === "error") totalErrors++;
932
+ else totalWarnings++;
933
+ }
934
+
935
+ return { canvases, channelDiagnostics, memoryDiagnostics, modelDiagnostics, totalErrors, totalWarnings };
936
+ }