1- import { MCPTaskRegistry } from '@idl/mcp/tasks' ;
1+ import { ITaskInformation , MCPTaskRegistry } from '@idl/mcp/tasks' ;
22import { IDLTypeHelper } from '@idl/parsing/type-parser' ;
33import { ENVIModelerEdge , ENVIModelerNode } from '@idl/types/envi/modeler' ;
44import { IDL_TYPE_LOOKUP } from '@idl/types/idl-data-types' ;
55
6+ import { BuildConnectionMap } from './helpers/build-connection-map' ;
67import {
78 SINK_TYPES ,
89 SOURCE_TYPES ,
@@ -16,9 +17,11 @@ export function ValidateENVIModelerNodes(
1617 edges : ENVIModelerEdge [ ] ,
1718 registry : MCPTaskRegistry ,
1819) : string [ ] {
19- // ---- validate edges reference valid node ids and obey source/sink rules
20- const nodeIds = new Set ( nodes . map ( ( n ) => n . id ) ) ;
21- const nodeTypeById = new Map ( nodes . map ( ( n ) => [ n . id , n . type ] ) ) ;
20+ /** Create map for nodes */
21+ const nodeMap : { [ key : string ] : ENVIModelerNode } = { } ;
22+ for ( let i = 0 ; i < nodes . length ; i ++ ) {
23+ nodeMap [ nodes [ i ] . id ] = nodes [ i ] ;
24+ }
2225
2326 /** Track validation errors */
2427 const errors : string [ ] = [ ] ;
@@ -30,16 +33,17 @@ export function ValidateENVIModelerNodes(
3033 case 'task' :
3134 // make sure there is a name
3235 if ( node . task_name ) {
33- // make sure it is a known task
34- if ( ! registry . hasTask ( node . task_name ) ) {
36+ /** Get task information and validate a bit */
37+ const info = registry . getTaskDetail ( node . task_name ) ;
38+
39+ // verify that we have a known task
40+ if ( ! info ) {
3541 errors . push (
3642 `Node "${ node . id } " references an unknown task "${ node . task_name } "` ,
3743 ) ;
44+ continue ;
3845 }
3946
40- /** Get task information and validate a bit */
41- const info = registry . getTaskDetail ( node . task_name ) ;
42-
4347 // validate input parameters
4448 for ( const param of Object . keys ( node . static_input || { } ) ) {
4549 if ( ! ( param . toLowerCase ( ) in info . structure . meta . props ) ) {
@@ -58,80 +62,195 @@ export function ValidateENVIModelerNodes(
5862 }
5963 }
6064
61- // check each edge
62- for ( const edge of edges ) {
63- if ( ! nodeIds . has ( edge . from ) ) {
65+ /**
66+ * Validate all of our edges
67+ */
68+ for ( let i = 0 ; i < edges . length ; i ++ ) {
69+ const edge = edges [ i ] ;
70+
71+ // verify that we have correct IDs for connections
72+ if ( ! ( edge . from in nodeMap ) ) {
6473 errors . push ( `Edge references unknown source node id "${ edge . from } "` ) ;
6574 }
66- if ( ! nodeIds . has ( edge . to ) ) {
75+ if ( ! ( edge . to in nodeMap ) ) {
6776 errors . push ( `Edge references unknown target node id "${ edge . to } "` ) ;
6877 }
6978
70- const fromType = nodeTypeById . get ( edge . from ) ;
71- if ( fromType && SINK_TYPES . has ( fromType ) ) {
79+ // extract nodes
80+ const from = nodeMap [ edge . from ] ;
81+ const to = nodeMap [ edge . to ] ;
82+
83+ // verify we arent coming from a sink
84+ if ( SINK_TYPES . has ( from . type ) ) {
7285 errors . push (
73- `Node "${ edge . from } " (type "${ fromType } ") is a sink-only node and cannot be an edge source` ,
86+ `Node "${ edge . from } " (type "${ from . type } ") is a sink-only node and cannot be an edge source` ,
7487 ) ;
7588 }
76- if ( fromType === 'aggregator' ) {
77- edge . from_parameters = [ 'output' ] ;
78- }
7989
80- const toType = nodeTypeById . get ( edge . to ) ;
81- if ( toType && SOURCE_TYPES . has ( toType ) ) {
90+ // verify type we arent going to a source
91+ if ( SOURCE_TYPES . has ( to . type ) ) {
8292 errors . push (
83- `Node "${ edge . to } " (type "${ toType } ") is a source-only node and cannot be an edge target` ,
93+ `Node "${ edge . to } " (type "${ to . type } ") is a source-only node and cannot be an edge target` ,
8494 ) ;
8595 }
86- }
8796
88- // Validate that task input parameters receiving multiple edges are array-typed.
89- // When a parameter has 2+ incoming edges, InjectAggregatorNodes will insert an
90- // aggregator — but only if the target parameter actually accepts an array.
91- const inputEdgeCount = new Map < string , number > ( ) ;
97+ /** Task info for our from node */
98+ let fromInfo : ITaskInformation | undefined ;
9299
93- for ( const edge of edges ) {
94- if ( nodeTypeById . get ( edge . to ) !== 'task' ) {
95- continue ;
96- }
100+ /** Errors for from parameters */
101+ const fromErrs : string [ ] = [ ] ;
97102
98- if ( edge . to_parameters . length === 0 || edge . to_parameters [ 0 ] === '' ) {
99- continue ;
100- }
103+ /**
104+ * Validate from task parameters
105+ */
106+ if ( from . type === 'task' ) {
107+ // make sure we have a task name
108+ if ( from . task_name ) {
109+ fromInfo = registry . getTaskDetail ( from . task_name ) ;
110+ }
101111
102- const key = `${ edge . to } ::${ edge . to_parameters . join ( ',' ) } ` ;
103- inputEdgeCount . set ( key , ( inputEdgeCount . get ( key ) ?? 0 ) + 1 ) ;
104- }
112+ // make sure we have info
113+ if ( fromInfo ) {
114+ // validate from parameters
115+ for ( let j = 0 ; j < edge . from_parameters . length ; j ++ ) {
116+ const fromParam = edge . from_parameters [ j ] ;
117+ const fromProp =
118+ fromInfo . structure . meta . props [ fromParam . toLowerCase ( ) ] ;
105119
106- for ( const [ key , count ] of inputEdgeCount ) {
107- if ( count <= 1 ) {
108- continue ;
120+ // make sure it is a real parameter we are coming from
121+ if ( ! fromProp ) {
122+ fromErrs . push (
123+ ` "${ fromParam } " is not a known parameter of "${ from . task_name } "` ,
124+ ) ;
125+ continue ;
126+ }
127+
128+ // make sure output
129+ if ( ! ( fromProp . direction === 'out' ) ) {
130+ fromErrs . push (
131+ ` "${ fromParam } " is not an output parameter and cannot be connected` ,
132+ ) ;
133+ }
134+ }
135+ }
109136 }
110137
111- const separatorIdx = key . indexOf ( '::' ) ;
112- const toId = key . slice ( 0 , separatorIdx ) ;
113- const paramName = key . slice ( separatorIdx + 2 ) ;
138+ /** Task info for our to node */
139+ let toInfo : ITaskInformation | undefined ;
114140
115- const toNode = nodes . find ( ( n ) => n . id === toId ) ;
116- if ( ! toNode || toNode . type !== 'task' || ! toNode . task_name ) {
117- continue ;
141+ // track to errors
142+ const toErrors : string [ ] = [ ] ;
143+
144+ /**
145+ * Validate to task parameters
146+ */
147+ if ( to . type === 'task' ) {
148+ // make sure we have a task name
149+ if ( to . task_name ) {
150+ toInfo = registry . getTaskDetail ( to . task_name ) ;
151+ }
152+
153+ // make sure we have info
154+ if ( toInfo ) {
155+ // validate ti parameters
156+ for ( let j = 0 ; j < edge . to_parameters . length ; j ++ ) {
157+ const toParam = edge . to_parameters [ j ] ;
158+ const toProp = toInfo . structure . meta . props [ toParam . toLowerCase ( ) ] ;
159+
160+ // make sure it is a real parameter we are coming from
161+ if ( ! toProp ) {
162+ toErrors . push (
163+ ` "${ toParam } " is not a known parameter of "${ to . task_name } "` ,
164+ ) ;
165+ continue ;
166+ }
167+
168+ // make sure output
169+ if ( ! ( toProp . direction === 'in' ) ) {
170+ toErrors . push (
171+ ` "${ toParam } " is not an input parameter and cannot be connected` ,
172+ ) ;
173+ }
174+ }
175+ }
118176 }
119177
120- if ( ! registry . hasTask ( toNode . task_name ) ) {
121- continue ;
178+ // format errors
179+ if ( fromErrs . length > 0 ) {
180+ errors . push (
181+ `The "from_parameters" property on edge node ${ i } has problems that need resolved:` ,
182+ ) ;
183+ errors . push ( ...fromErrs ) ;
122184 }
123185
124- const info = registry . getTaskDetail ( toNode . task_name ) ;
125- const prop = info . structure . meta . props [ paramName . toLowerCase ( ) ] ;
186+ // format errors
187+ if ( toErrors . length > 0 ) {
188+ errors . push (
189+ `The "to_parameters" property on edge node ${ i } has problems that need resolved:` ,
190+ ) ;
191+ errors . push ( ...toErrors ) ;
192+ }
193+
194+ /**
195+ * @TODO add in logic to validate compatibility of parameters
196+ *
197+ * Maybe leave this to ENVI, but without this feedback the LLM may
198+ * get things wrong
199+ */
200+ // if (fromInfo && toInfo) {
201+ // }
202+ }
126203
127- if ( ! prop ) {
204+ /**
205+ * Build connection map to validate array-based input parameters
206+ */
207+ const map = BuildConnectionMap ( nodes , edges ) ;
208+
209+ // get IDs
210+ const ids = Object . keys ( map ) ;
211+
212+ /**
213+ * Validate array connections that we will automatically add aggregators to so
214+ * so that we actually have arrays
215+ */
216+ for ( let i = 0 ; i < ids . length ; i ++ ) {
217+ // skip if not a task
218+ if ( nodeMap [ ids [ i ] ] ?. type !== 'task' ) {
128219 continue ;
129220 }
130221
131- if ( ! IDLTypeHelper . isType ( prop . type , IDL_TYPE_LOOKUP . ARRAY ) ) {
132- errors . push (
133- `Node "${ toId } " parameter "${ paramName } " receives ${ count } edges but is not an array type; aggregator injection is not valid` ,
134- ) ;
222+ // get parameter names to verify
223+ const byParam = map [ ids [ i ] ] ;
224+
225+ // get parameters
226+ const params = Object . keys ( byParam ) ;
227+
228+ // process each parameter
229+ for ( let j = 0 ; j < params . length ; j ++ ) {
230+ const param = params [ j ] ;
231+ const connections = byParam [ param ] ;
232+
233+ // check for multiple connections
234+ if ( connections . length > 1 ) {
235+ const info = registry . getTaskDetail ( nodeMap [ ids [ i ] ] ?. task_name || '' ) ;
236+ if ( info ) {
237+ // check if unknown parameter
238+ if ( ! ( param in info . structure . meta . props ) ) {
239+ continue ;
240+ }
241+
242+ if (
243+ ! IDLTypeHelper . isType (
244+ info . structure . meta . props [ param ] . type ,
245+ IDL_TYPE_LOOKUP . ARRAY ,
246+ )
247+ ) {
248+ errors . push (
249+ `The node "${ ids [ i ] } " has multiple inputs for the task the parameter "${ param } ", but this parameter is not an array type and does not accept multiple inputs` ,
250+ ) ;
251+ }
252+ }
253+ }
135254 }
136255 }
137256
0 commit comments