Skip to content

Commit 1f23392

Browse files
WIP: Significant rewrite of validating ENVI modeler workflow
1 parent 67c58e9 commit 1f23392

1 file changed

Lines changed: 174 additions & 55 deletions

File tree

libs/envi/modeler/src/lib/validate-envi-modeler-nodes.ts

Lines changed: 174 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { MCPTaskRegistry } from '@idl/mcp/tasks';
1+
import { ITaskInformation, MCPTaskRegistry } from '@idl/mcp/tasks';
22
import { IDLTypeHelper } from '@idl/parsing/type-parser';
33
import { ENVIModelerEdge, ENVIModelerNode } from '@idl/types/envi/modeler';
44
import { IDL_TYPE_LOOKUP } from '@idl/types/idl-data-types';
55

6+
import { BuildConnectionMap } from './helpers/build-connection-map';
67
import {
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

Comments
 (0)