elsabro 7.3.0 → 7.3.2

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.
@@ -64,15 +64,16 @@ function buildGraph(flowDefinition) {
64
64
 
65
65
  /**
66
66
  * Validate that all node references (next, routes, true, false, onMaxIterations,
67
- * onError) point to existing nodes.
67
+ * onError) point to existing nodes, and detect orphaned nodes.
68
68
  *
69
- * @param {{ nodes: Map<string, object> }} graph
69
+ * @param {{ nodes: Map<string, object>, entryNode: string }} graph
70
70
  * @returns {{ valid: boolean, errors: string[] }}
71
71
  */
72
72
  function validateGraph(graph) {
73
73
  const errors = [];
74
74
  const nodeIds = new Set(graph.nodes.keys());
75
75
 
76
+ // 1. Check for dangling references (invalid routes)
76
77
  for (const [id, node] of graph.nodes) {
77
78
  const refs = getNextNodes(node);
78
79
  for (const ref of refs) {
@@ -82,6 +83,33 @@ function validateGraph(graph) {
82
83
  }
83
84
  }
84
85
 
86
+ // 2. Detect orphaned nodes (nodes that are never reachable from entry)
87
+ const reachable = new Set();
88
+ const queue = [graph.entryNode];
89
+
90
+ while (queue.length > 0) {
91
+ const currentId = queue.shift();
92
+ if (reachable.has(currentId)) continue;
93
+
94
+ reachable.add(currentId);
95
+ const node = graph.nodes.get(currentId);
96
+ if (!node) continue; // Skip if node doesn't exist (handled by dangling ref check)
97
+
98
+ const nextNodes = getNextNodes(node);
99
+ for (const nextId of nextNodes) {
100
+ if (!reachable.has(nextId) && nodeIds.has(nextId)) {
101
+ queue.push(nextId);
102
+ }
103
+ }
104
+ }
105
+
106
+ // Find orphaned nodes
107
+ for (const nodeId of nodeIds) {
108
+ if (!reachable.has(nodeId)) {
109
+ errors.push(`Orphaned node "${nodeId}" is unreachable from entry point`);
110
+ }
111
+ }
112
+
85
113
  return { valid: errors.length === 0, errors };
86
114
  }
87
115
 
@@ -73,7 +73,17 @@ registerFunction('collectErrors', (context) => {
73
73
  registerFunction('hasCriticalIssues', (context, obj) => {
74
74
  if (!obj) return false;
75
75
  const json = typeof obj === 'string' ? obj : JSON.stringify(obj);
76
- return json.includes('"critical"') || json.includes('"blocking"');
76
+
77
+ // Enhanced pattern detection for critical issues
78
+ const criticalPatterns = [
79
+ '"critical"',
80
+ '"blocking"',
81
+ '"P0"',
82
+ '"MUST_FIX"',
83
+ '"URGENT"'
84
+ ];
85
+
86
+ return criticalPatterns.some(pattern => json.includes(pattern));
77
87
  });
78
88
 
79
89
  registerFunction('generateSummary', (context) => {
@@ -144,92 +154,162 @@ function parseLiteral(str) {
144
154
  }
145
155
 
146
156
  /**
147
- * Resolve a single expression string (the content inside {{ }}).
148
- *
149
- * @param {string} expr – raw expression without {{ }}
150
- * @param {object} context { inputs, nodes, steps, state }
151
- * @returns {*}
157
+ * Handle filter syntax: "value | filterName"
158
+ * @param {string} expr
159
+ * @param {object} context
160
+ * @returns {*|null} - Filtered value or null if no match
152
161
  */
153
- function resolveExpression(expr, context) {
154
- const trimmed = expr.trim();
155
-
156
- // 1. Check for filter syntax: "value | filterName"
157
- // Only match pipes that are NOT inside || (fallback operator)
158
- const filterMatch = trimmed.match(/^(.+?)\s*\|\s*(?!\|)(\w+)\s*$/);
159
- if (filterMatch) {
160
- const innerValue = resolveExpression(filterMatch[1], context);
161
- const filterName = filterMatch[2];
162
- const filter = _filters.get(filterName);
163
- if (!filter) {
164
- throw new TemplateError(`Unknown filter: "${filterName}"`);
165
- }
166
- return filter(innerValue);
162
+ function tryResolveFilter(expr, context) {
163
+ // Only match pipes that are NOT inside || (fallback operator)
164
+ const filterMatch = expr.match(/^(.+?)\s*\|\s*(?!\|)(\w+)\s*$/);
165
+ if (!filterMatch) return null;
166
+
167
+ const innerValue = resolveExpression(filterMatch[1], context);
168
+ const filterName = filterMatch[2];
169
+ const filter = _filters.get(filterName);
170
+ if (!filter) {
171
+ throw new TemplateError(`Unknown filter: "${filterName}"`);
167
172
  }
173
+ return filter(innerValue);
174
+ }
168
175
 
169
- // 2. Check for fallback operator: "expr || default"
170
- const fallbackMatch = trimmed.match(/^(.+?)\s*\|\|\s*(.+)$/);
171
- if (fallbackMatch) {
172
- const primary = resolveExpression(fallbackMatch[1], context);
173
- if (primary != null && primary !== '') return primary;
174
- // Try to resolve the default as an expression or literal
175
- const fallbackLiteral = parseLiteral(fallbackMatch[2]);
176
- if (fallbackLiteral.matched) return fallbackLiteral.value;
177
- return resolveExpression(fallbackMatch[2], context);
178
- }
176
+ /**
177
+ * Handle fallback operator: "expr || default"
178
+ * @param {string} expr
179
+ * @param {object} context
180
+ * @returns {*|null} - Primary or fallback value, or null if no match
181
+ */
182
+ function tryResolveFallback(expr, context) {
183
+ const fallbackMatch = expr.match(/^(.+?)\s*\|\|\s*(.+)$/);
184
+ if (!fallbackMatch) return null;
185
+
186
+ const primary = resolveExpression(fallbackMatch[1], context);
187
+ if (primary != null && primary !== '') return primary;
188
+
189
+ // Try to resolve the default as a literal or expression
190
+ const fallbackLiteral = parseLiteral(fallbackMatch[2]);
191
+ if (fallbackLiteral.matched) return fallbackLiteral.value;
192
+ return resolveExpression(fallbackMatch[2], context);
193
+ }
194
+
195
+ /**
196
+ * Handle negation: "!expr"
197
+ * @param {string} expr
198
+ * @param {object} context
199
+ * @returns {*|null} - Negated value or null if no match
200
+ */
201
+ function tryResolveNegation(expr, context) {
202
+ if (!expr.startsWith('!')) return null;
203
+ const inner = resolveExpression(expr.slice(1), context);
204
+ return !inner;
205
+ }
179
206
 
180
- // 3. Check for negation: "!expr"
181
- if (trimmed.startsWith('!')) {
182
- const inner = resolveExpression(trimmed.slice(1), context);
183
- return !inner;
207
+ /**
208
+ * Handle boolean && expression
209
+ * @param {string} expr
210
+ * @param {object} context
211
+ * @returns {*|null} - Boolean result or null if no match
212
+ */
213
+ function tryResolveAnd(expr, context) {
214
+ if (!expr.includes('&&')) return null;
215
+
216
+ const parts = expr.split('&&').map(p => p.trim());
217
+ return parts.every(part => {
218
+ const val = resolveExpression(part, context);
219
+ return !!val;
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Handle comparison operators: ===, !==
225
+ * @param {string} expr
226
+ * @param {object} context
227
+ * @returns {*|null} - Comparison result or null if no match
228
+ */
229
+ function tryResolveComparison(expr, context) {
230
+ const compMatch = expr.match(/^(.+?)\s*(===|!==)\s*(.+)$/);
231
+ if (!compMatch) return null;
232
+
233
+ const left = resolveExpression(compMatch[1], context);
234
+ const right = resolveExpression(compMatch[3], context);
235
+ if (compMatch[2] === '===') return left === right;
236
+ if (compMatch[2] === '!==') return left !== right;
237
+ return null;
238
+ }
239
+
240
+ /**
241
+ * Handle function call: "functionName(args)"
242
+ * @param {string} expr
243
+ * @param {object} context
244
+ * @returns {*|null} - Function result or null if no match
245
+ */
246
+ function tryResolveFunctionCall(expr, context) {
247
+ const funcMatch = expr.match(/^(\w+)\(([^)]*)\)$/);
248
+ if (!funcMatch) return null;
249
+
250
+ const funcName = funcMatch[1];
251
+ const fn = _functions.get(funcName);
252
+ if (!fn) {
253
+ throw new TemplateError(`Unknown function: "${funcName}"`);
184
254
  }
185
255
 
186
- // 4. Check for boolean && expression
187
- if (trimmed.includes('&&')) {
188
- const parts = trimmed.split('&&').map(p => p.trim());
189
- return parts.every(part => {
190
- const val = resolveExpression(part, context);
191
- return !!val;
192
- });
256
+ const argsStr = funcMatch[2].trim();
257
+ if (argsStr === '') {
258
+ return fn(context);
193
259
  }
194
260
 
195
- // 5. Check for comparison operators: ===, !==
196
- const compMatch = trimmed.match(/^(.+?)\s*(===|!==)\s*(.+)$/);
197
- if (compMatch) {
198
- const left = resolveExpression(compMatch[1], context);
199
- const right = resolveExpression(compMatch[3], context);
200
- if (compMatch[2] === '===') return left === right;
201
- if (compMatch[2] === '!==') return left !== right;
261
+ // Parse argument: could be a path reference or a quoted string
262
+ const argLiteral = parseLiteral(argsStr);
263
+ if (argLiteral.matched) {
264
+ return fn(context, argLiteral.value);
202
265
  }
203
266
 
204
- // 6. Check for function call: "functionName(args)"
205
- const funcMatch = trimmed.match(/^(\w+)\(([^)]*)\)$/);
206
- if (funcMatch) {
207
- const funcName = funcMatch[1];
208
- const fn = _functions.get(funcName);
209
- if (!fn) {
210
- throw new TemplateError(`Unknown function: "${funcName}"`);
211
- }
212
- const argsStr = funcMatch[2].trim();
213
- if (argsStr === '') {
214
- return fn(context);
215
- }
216
- // Parse argument: could be a path reference or a quoted string
217
- const argLiteral = parseLiteral(argsStr);
218
- if (argLiteral.matched) {
219
- return fn(context, argLiteral.value);
220
- }
221
- // Resolve as expression (path)
222
- const argValue = resolveExpression(argsStr, context);
223
- return fn(context, argValue);
267
+ // Resolve as expression (path)
268
+ const argValue = resolveExpression(argsStr, context);
269
+ return fn(context, argValue);
270
+ }
271
+
272
+ /**
273
+ * Resolve a single expression string (the content inside {{ }}).
274
+ *
275
+ * Delegates to specialized handlers in order:
276
+ * 1. Filter syntax: "value | filterName"
277
+ * 2. Fallback operator: "expr || default"
278
+ * 3. Negation: "!expr"
279
+ * 4. Boolean AND: "a && b"
280
+ * 5. Comparison: "a === b", "a !== b"
281
+ * 6. Function call: "functionName(args)"
282
+ * 7. Literal value: true, false, number, string, [], {}
283
+ * 8. Path resolution: "inputs.task", "nodes.X.outputs.Y"
284
+ *
285
+ * @param {string} expr – raw expression without {{ }}
286
+ * @param {object} context – { inputs, nodes, steps, state }
287
+ * @returns {*}
288
+ */
289
+ function resolveExpression(expr, context) {
290
+ const trimmed = expr.trim();
291
+
292
+ // Try each expression type in order (most specific to least specific)
293
+ const handlers = [
294
+ tryResolveFilter,
295
+ tryResolveFallback,
296
+ tryResolveNegation,
297
+ tryResolveAnd,
298
+ tryResolveComparison,
299
+ tryResolveFunctionCall
300
+ ];
301
+
302
+ for (const handler of handlers) {
303
+ const result = handler(trimmed, context);
304
+ if (result !== null) return result;
224
305
  }
225
306
 
226
- // 7. Literal value
307
+ // Try literal value
227
308
  const literal = parseLiteral(trimmed);
228
309
  if (literal.matched) return literal.value;
229
310
 
230
- // 8. Path resolution (the most common case)
231
- const value = resolvePath(context, trimmed);
232
- return value;
311
+ // Default: path resolution (most common case)
312
+ return resolvePath(context, trimmed);
233
313
  }
234
314
 
235
315
  /**