@onlineapps/conn-orch-orchestrator 1.0.31 → 1.0.33

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-orchestrator",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Workflow orchestration connector for OA Drive - handles message routing and workflow execution",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -22,14 +22,13 @@
22
22
  "dependencies": {
23
23
  "@onlineapps/conn-base-monitoring": "^1.0.0",
24
24
  "@onlineapps/conn-infra-mq": "^1.1.0",
25
- "@onlineapps/conn-orch-api-mapper": "^1.0.8",
26
- "@onlineapps/conn-orch-cookbook": "^2.0.0",
27
25
  "@onlineapps/conn-orch-registry": "^1.1.4",
28
- "@onlineapps/cookbook-core": "^2.1.2"
26
+ "@onlineapps/conn-orch-cookbook": "^2.0.0",
27
+ "@onlineapps/conn-orch-api-mapper": "^1.0.0"
29
28
  },
30
29
  "devDependencies": {
31
- "eslint": "^8.30.0",
32
30
  "jest": "^29.5.0",
31
+ "eslint": "^8.30.0",
33
32
  "jsdoc-to-markdown": "^8.0.0"
34
33
  },
35
34
  "jest": {
@@ -51,6 +51,16 @@ class WorkflowOrchestrator {
51
51
  }
52
52
  }
53
53
 
54
+ /**
55
+ * Get step identifier (supports both V1 'id' and V2 'step_id')
56
+ * @private
57
+ * @param {Object} step - Step object
58
+ * @returns {string} Step identifier
59
+ */
60
+ _getStepId(step) {
61
+ return step?.step_id || step?.id;
62
+ }
63
+
54
64
  /**
55
65
  * Process a workflow message
56
66
  * @async
@@ -75,10 +85,7 @@ class WorkflowOrchestrator {
75
85
  const { workflow_id, cookbook: cookbookDef, current_step, context } = message;
76
86
 
77
87
  try {
78
- // FAIL-FAST: Validate V2 format
79
- this._validateV2Format(cookbookDef);
80
-
81
- // Validate against JSON Schema (V2 schema - steps as object)
88
+ // Validate cookbook structure
82
89
  this.cookbook.validateCookbook(cookbookDef);
83
90
 
84
91
  // Ensure delivery configuration is preserved in context (required by Delivery Dispatcher)
@@ -89,14 +96,11 @@ class WorkflowOrchestrator {
89
96
  delivery: context?.delivery || cookbookDef?.delivery || { handler: 'none' }
90
97
  };
91
98
 
92
- // Find current step (V2: steps is an object keyed by step_id)
93
- const step = cookbookDef.steps[current_step];
99
+ // Find current step (supports both V1 'id' and V2 'step_id')
100
+ const step = cookbookDef.steps.find(s => this._getStepId(s) === current_step);
94
101
  if (!step) {
95
- throw new Error(`Step not found: ${current_step}. Available steps: ${Object.keys(cookbookDef.steps).join(', ')}`);
102
+ throw new Error(`Step not found: ${current_step}`);
96
103
  }
97
-
98
- // Get step_id (V2)
99
- const stepId = step.step_id;
100
104
 
101
105
  // Check if this step is for this service
102
106
  if (step.type === 'task' && step.service !== serviceName) {
@@ -118,7 +122,7 @@ class WorkflowOrchestrator {
118
122
  result = await this.cache.get(cacheKey);
119
123
 
120
124
  if (result) {
121
- this.logger.info('Cache hit', { step: stepId, cacheKey });
125
+ this.logger.info('Cache hit', { step: this._getStepId(step), cacheKey });
122
126
  } else {
123
127
  // Execute the step
124
128
  result = await this._executeStep(step, enrichedContext, cookbookDef);
@@ -129,55 +133,29 @@ class WorkflowOrchestrator {
129
133
  result = await this._executeStep(step, enrichedContext, cookbookDef);
130
134
  }
131
135
 
132
- // Update context with result - steps as OBJECT (V2 format, keyed by step_id)
133
- // Each step preserves its definition (step_id, type, service, operation, input) and adds output
134
- const stepDefinition = cookbookDef.steps[current_step];
135
-
136
- // Initialize steps object from cookbook if not present
137
- // V2: steps is an object keyed by step_id
138
- let existingSteps = enrichedContext.steps || {};
136
+ // Update context with result - steps as ARRAY (consistent with cookbook.steps)
137
+ // Each step preserves its definition (id/step_id, type, service, operation, input) and adds output
138
+ const currentIndex = cookbookDef.steps.findIndex(s => this._getStepId(s) === current_step);
139
+ const stepDefinition = cookbookDef.steps[currentIndex];
139
140
 
140
- // Initialize from cookbook if empty
141
- if (Object.keys(existingSteps).length === 0) {
142
- Object.entries(cookbookDef.steps).forEach(([key, s]) => {
143
- existingSteps[key] = { ...s };
144
- });
145
- }
141
+ // Initialize steps array from cookbook if not present
142
+ const existingSteps = Array.isArray(enrichedContext.steps)
143
+ ? [...enrichedContext.steps]
144
+ : cookbookDef.steps.map(s => ({ ...s })); // Deep copy of step definitions
146
145
 
147
- // Update the current step with output (preserve step_id, type, service, operation, input)
148
- existingSteps[stepId] = {
149
- ...stepDefinition, // step_id, type, service, operation, input
146
+ // Update the current step with output (preserve id, type, service, operation, input)
147
+ existingSteps[currentIndex] = {
148
+ ...stepDefinition, // id, type, service, operation, input
150
149
  output: result, // Add output from operation
151
150
  status: 'completed',
152
151
  completed_at: new Date().toISOString()
153
152
  };
154
153
 
155
- // === STRUCTURED LOG: Context update ===
156
- console.log(`[WorkflowOrchestrator:CONTEXT_UPDATE] ${JSON.stringify({
157
- layer: 'orchestrator',
158
- workflow_id: workflow_id,
159
- step_id: stepId,
160
- stored_output: {
161
- _descriptor: result?._descriptor,
162
- type: result?.type,
163
- storage_ref: result?.storage_ref,
164
- outputKeys: result && typeof result === 'object' ? Object.keys(result) : null
165
- },
166
- timestamp: new Date().toISOString()
167
- })}`);
168
-
169
-
170
154
  const updatedContext = {
171
155
  ...enrichedContext,
172
- steps: existingSteps // Object keyed by step_id
156
+ steps: existingSteps
173
157
  };
174
158
 
175
- // V2: Get step order from object keys (JavaScript preserves insertion order for string keys)
176
- const stepKeys = Object.keys(cookbookDef.steps);
177
- const currentIndex = stepKeys.indexOf(current_step);
178
- const nextStepKey = stepKeys[currentIndex + 1];
179
- const nextStep = nextStepKey ? cookbookDef.steps[nextStepKey] : null;
180
-
181
159
  // Publish workflow.progress AFTER execution (so we have step output)
182
160
  const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
183
161
  try {
@@ -204,13 +182,15 @@ class WorkflowOrchestrator {
204
182
  });
205
183
  }
206
184
 
185
+ // Find next step (currentIndex already defined above)
186
+ const nextStep = cookbookDef.steps[currentIndex + 1];
187
+
207
188
  if (nextStep) {
208
189
  // Route to next step
209
- const nextStepId = nextStep.step_id;
210
- await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id, nextStepId);
190
+ await this._routeToNextStep(nextStep, cookbookDef, updatedContext, workflow_id);
211
191
  } else {
212
192
  // Workflow completed - pass serviceName, cookbook and last step info for monitoring
213
- await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName, stepId, currentIndex);
193
+ await this._completeWorkflow(workflow_id, updatedContext, cookbookDef, serviceName, current_step, currentIndex);
214
194
  }
215
195
 
216
196
  return {
@@ -295,78 +275,25 @@ class WorkflowOrchestrator {
295
275
  * @returns {Promise<Object>} API call result
296
276
  */
297
277
  async _executeTaskStep(step, context) {
298
- // FAIL-FAST: Verify context exists
299
- if (!context) {
300
- const error = new Error(`Context is missing for step ${step.step_id}`);
301
- this.logger.error('[WorkflowOrchestrator] [FAIL-FAST] Missing context', { step_id: step.step_id });
302
- throw error;
303
- }
278
+ const stepId = this._getStepId(step);
279
+ // Resolve variable references in step.input (e.g. ${steps[0].output.message})
280
+ console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${stepId} input BEFORE:`, JSON.stringify(step.input));
281
+ console.log(`[WorkflowOrchestrator] [RESOLVE] Context keys:`, Object.keys(context || {}));
282
+ console.log(`[WorkflowOrchestrator] [RESOLVE] Context.input_file:`, context?.input_file);
283
+ console.log(`[WorkflowOrchestrator] [RESOLVE] Context.steps:`, JSON.stringify(context.steps?.map(s => ({ id: this._getStepId(s), output: s.output }))));
304
284
 
305
- // Debug: log context keys and api_input
306
- console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} - context keys:`, Object.keys(context || {}));
307
-
308
- // FAIL-FAST: Log api_input state (but don't fail if missing - it might not be needed)
309
- if (context.api_input === undefined) {
310
- console.warn(`[WorkflowOrchestrator] [WARNING] Step ${step.step_id} - context.api_input is undefined - template references like {{api_input.field}} will fail`);
311
- } else if (context.api_input === null) {
312
- console.warn(`[WorkflowOrchestrator] [WARNING] Step ${step.step_id} - context.api_input is null - template references like {{api_input.field}} will fail`);
313
- } else if (typeof context.api_input !== 'object' || Array.isArray(context.api_input)) {
314
- const error = new Error(`Invalid api_input type for step ${step.step_id}: expected object, got ${typeof context.api_input}`);
315
- console.error(`[WorkflowOrchestrator] [FAIL-FAST] Invalid api_input type:`, {
316
- step_id: step.step_id,
317
- api_input_type: typeof context.api_input,
318
- api_input_value: context.api_input,
319
- is_array: Array.isArray(context.api_input)
320
- });
321
- throw error;
322
- } else {
323
- console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} - context.api_input:`, JSON.stringify(context.api_input));
324
- }
325
-
326
- console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} - context.steps keys:`,
327
- context.steps ? Object.keys(context.steps) : 'NO STEPS');
328
-
329
- // Resolve variable references in step.input (e.g. {{steps.step_id.output.field}})
330
- console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} input BEFORE:`, JSON.stringify(step.input));
331
285
  const resolvedInput = this._resolveInputReferences(step.input, context);
332
- console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} input AFTER:`, JSON.stringify(resolvedInput));
333
-
334
- // FAIL-FAST: Check if template references were resolved
335
- const inputStr = JSON.stringify(resolvedInput);
336
- if (inputStr.includes('{{api_input.') || inputStr.includes('${api_input.')) {
337
- console.error(`[WorkflowOrchestrator] [FAIL-FAST] Unresolved api_input template in step ${step.step_id}:`, {
338
- step_id: step.step_id,
339
- resolved_input: resolvedInput,
340
- context_api_input: context.api_input,
341
- context_keys: Object.keys(context)
342
- });
343
- throw new Error(`Template reference to api_input not resolved in step ${step.step_id} - check that api_input is in context`);
344
- }
286
+ console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${stepId} input AFTER:`, JSON.stringify(resolvedInput));
345
287
 
346
288
  // Use API mapper to call the service with resolved input
347
289
  const result = await this.apiMapper.callOperation(
348
- step.operation,
290
+ step.operation || stepId,
349
291
  resolvedInput,
350
292
  context
351
293
  );
352
294
 
353
- // === STRUCTURED LOG: Step result ===
354
- console.log(`[WorkflowOrchestrator:STEP_RESULT] ${JSON.stringify({
355
- layer: 'orchestrator',
356
- step_id: step.step_id,
357
- service: step.service,
358
- operation: step.operation,
359
- result: {
360
- _descriptor: result?._descriptor,
361
- type: result?.type,
362
- storage_ref: result?.storage_ref,
363
- resultKeys: result && typeof result === 'object' ? Object.keys(result) : null
364
- },
365
- timestamp: new Date().toISOString()
366
- })}`);
367
-
368
295
  this.logger.info(`Task step executed`, {
369
- step: step.step_id,
296
+ step: stepId,
370
297
  service: step.service,
371
298
  operation: step.operation,
372
299
  inputResolved: !!resolvedInput
@@ -377,9 +304,7 @@ class WorkflowOrchestrator {
377
304
 
378
305
  /**
379
306
  * Resolve variable references in input object
380
- * Supports both syntaxes:
381
- * - {{steps.step_id.output.field}}, {{api_input.field}} (preferred, V2)
382
- * - ${steps[N].output.field}, ${api_input.field} (legacy)
307
+ * Supports ${steps[N].output.field}, ${api_input.field}, ${context.field}
383
308
  * @private
384
309
  * @param {Object} input - Input object with potential references
385
310
  * @param {Object} context - Current context with steps, api_input, etc
@@ -389,60 +314,12 @@ class WorkflowOrchestrator {
389
314
  if (!input) return input;
390
315
 
391
316
  const resolveValue = (value) => {
392
- if (typeof value === 'string') {
393
- // Special case: entire string is a single template reference
394
- // Return the resolved value directly (preserves objects, arrays, etc.)
395
- const singleRefMatch = value.match(/^\{\{([^}]+)\}\}$/);
396
- if (singleRefMatch) {
397
- const resolved = this._getValueFromPath(singleRefMatch[1].trim(), context);
398
- if (resolved !== undefined) {
399
- return resolved; // Return object/array directly
400
- }
401
- return value; // Return original if not resolved
402
- }
403
-
404
- // Legacy syntax: ${...}
405
- const singleLegacyMatch = value.match(/^\$\{([^}]+)\}$/);
406
- if (singleLegacyMatch) {
407
- const resolved = this._getValueFromPath(singleLegacyMatch[1].trim(), context);
408
- if (resolved !== undefined) {
409
- return resolved; // Return object/array directly
410
- }
411
- return value; // Return original if not resolved
412
- }
413
-
414
- let result = value;
415
-
416
- // Replace {{...}} references embedded in strings (V2 preferred syntax)
417
- if (result.includes('{{')) {
418
- result = result.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
419
- const resolved = this._getValueFromPath(path.trim(), context);
420
- if (resolved !== undefined) {
421
- // Stringify objects when embedded in larger strings
422
- if (typeof resolved === 'object') {
423
- return JSON.stringify(resolved);
424
- }
425
- return resolved;
426
- }
427
- return match;
428
- });
429
- }
430
-
431
- // Replace ${...} references embedded in strings (legacy syntax)
432
- if (result.includes('${')) {
433
- result = result.replace(/\$\{([^}]+)\}/g, (match, path) => {
434
- const resolved = this._getValueFromPath(path.trim(), context);
435
- if (resolved !== undefined) {
436
- if (typeof resolved === 'object') {
437
- return JSON.stringify(resolved);
438
- }
439
- return resolved;
440
- }
441
- return match;
442
- });
443
- }
444
-
445
- return result;
317
+ if (typeof value === 'string' && value.includes('${')) {
318
+ // Replace ${...} references
319
+ return value.replace(/\$\{([^}]+)\}/g, (match, path) => {
320
+ const resolved = this._getValueFromPath(path.trim(), context);
321
+ return resolved !== undefined ? resolved : match;
322
+ });
446
323
  }
447
324
  if (Array.isArray(value)) {
448
325
  return value.map(resolveValue);
@@ -461,34 +338,16 @@ class WorkflowOrchestrator {
461
338
  /**
462
339
  * Get value from dot/bracket notation path
463
340
  * @private
464
- * @param {string} path - Path like "steps.step_id.output.message" or "api_input.name"
341
+ * @param {string} path - Path like "steps[0].output.message" or "api_input.name"
465
342
  * @param {Object} context - Context object
466
343
  * @returns {*} Resolved value or undefined
467
344
  */
468
345
  _getValueFromPath(path, context) {
469
- // FAIL-FAST: Verify context exists
470
- if (!context) {
471
- console.error('[WorkflowOrchestrator] [FAIL-FAST] _getValueFromPath called with null/undefined context', { path });
472
- throw new Error(`Cannot resolve path '${path}': context is null or undefined`);
473
- }
474
-
475
- // V2: steps is an object keyed by step_id, not an array
476
- // Support both formats for backward compatibility during migration:
477
- // - steps.step_id.output.field (V2, preferred)
478
- // - steps[0].output.field (legacy, deprecated)
479
-
480
- // Normalize bracket notation to dot notation: steps[0] -> steps.0 (legacy)
481
- // But prefer step_id access: steps.step_id -> steps.step_id (V2)
482
- let normalized = path;
483
-
484
- // Handle legacy array access steps[N] - convert to numeric key
485
- // But only if steps is actually an array (fallback for old data)
486
- if (normalized.includes('[') && normalized.includes(']')) {
487
- normalized = normalized.replace(/\[(\d+)\]/g, '.$1');
488
- }
346
+ // Normalize bracket notation to dot notation: steps[0] -> steps.0
347
+ let normalized = path.replace(/\[(\d+)\]/g, '.$1');
489
348
 
490
349
  // Strip prefixes - we're already searching in context object
491
- // Supports: ${context.X}, ${api_input.X}, ${steps.step_id.X} (V2)
350
+ // Supports: ${context.X}, ${api_input.X}, ${steps[N].X}
492
351
  if (normalized.startsWith('context.')) {
493
352
  normalized = normalized.substring(8); // Remove 'context.'
494
353
  }
@@ -496,46 +355,9 @@ class WorkflowOrchestrator {
496
355
 
497
356
  const parts = normalized.split('.');
498
357
 
499
- // FAIL-FAST: Log api_input access attempts
500
- if (normalized.startsWith('api_input.')) {
501
- console.log(`[WorkflowOrchestrator] [GET_VALUE] Resolving api_input path: ${path} -> ${normalized}`);
502
- console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input exists:`, context.api_input !== undefined);
503
- console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input type:`, typeof context.api_input);
504
- if (context.api_input) {
505
- console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input keys:`, Object.keys(context.api_input));
506
- }
507
- }
508
-
509
358
  let current = context;
510
359
  for (const part of parts) {
511
- if (current === undefined || current === null) {
512
- // FAIL-FAST: Log failed resolution for api_input
513
- if (normalized.startsWith('api_input.')) {
514
- console.error(`[WorkflowOrchestrator] [FAIL-FAST] Failed to resolve api_input path: ${path}`, {
515
- path,
516
- normalized,
517
- parts,
518
- failed_at_part: part,
519
- current_type: typeof current,
520
- context_keys: Object.keys(context),
521
- context_has_api_input: 'api_input' in context,
522
- api_input_type: typeof context.api_input,
523
- api_input_keys: context.api_input ? Object.keys(context.api_input) : null
524
- });
525
- throw new Error(`Failed to resolve api_input path '${path}': '${part}' is undefined in context`);
526
- }
527
- return undefined;
528
- }
529
-
530
- // Special handling for steps: if accessing by numeric index and steps is object,
531
- // try to find step by index in cookbook (fallback for legacy)
532
- if (part === 'steps' && parts.length > 1) {
533
- current = current.steps;
534
- // If steps is object (V2), next part is step_id
535
- // If steps is array (legacy), next part is numeric index
536
- continue;
537
- }
538
-
360
+ if (current === undefined || current === null) return undefined;
539
361
  current = current[part];
540
362
  }
541
363
  return current;
@@ -570,7 +392,7 @@ class WorkflowOrchestrator {
570
392
  const result = await executor.executeStep(step);
571
393
 
572
394
  this.logger.info(`Control flow step executed`, {
573
- step: step.step_id,
395
+ step: this._getStepId(step),
574
396
  type: step.type
575
397
  });
576
398
 
@@ -585,9 +407,9 @@ class WorkflowOrchestrator {
585
407
  * @param {Object} cookbookDef - Full cookbook definition
586
408
  * @param {Object} context - Updated context
587
409
  * @param {string} workflow_id - Workflow ID
588
- * @param {string} nextStepId - Next step step_id (V2) or id (V1 fallback)
589
410
  */
590
- async _routeToNextStep(nextStep, cookbookDef, context, workflow_id, nextStepId) {
411
+ async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
412
+ const nextStepId = this._getStepId(nextStep);
591
413
  const message = {
592
414
  workflow_id,
593
415
  cookbook: cookbookDef,
@@ -645,21 +467,6 @@ class WorkflowOrchestrator {
645
467
  // Delivery Dispatcher requires: workflow_id, status, delivery (must be object, not null)
646
468
  const delivery = finalContext?.delivery || cookbookDef?.delivery || { handler: 'none' };
647
469
 
648
- // Extract api_output from the last completed step
649
- // steps is an object keyed by step_id in V2
650
- let api_output = null;
651
- if (lastStepId && finalContext?.steps?.[lastStepId]?.output) {
652
- api_output = finalContext.steps[lastStepId].output;
653
- } else if (finalContext?.steps) {
654
- // Fallback: find any step with output
655
- const stepKeys = Object.keys(finalContext.steps);
656
- for (const key of stepKeys) {
657
- if (finalContext.steps[key]?.output) {
658
- api_output = finalContext.steps[key].output;
659
- }
660
- }
661
- }
662
-
663
470
  // Build message in format expected by Delivery Dispatcher
664
471
  const workflowCompletedMessage = {
665
472
  workflow_id,
@@ -667,11 +474,10 @@ class WorkflowOrchestrator {
667
474
  service: serviceName, // Service that completed the workflow (for monitoring)
668
475
  step_id: lastStepId, // Last step ID for monitoring
669
476
  step_index: lastStepIndex, // Last step index for monitoring
670
- api_output: api_output, // Output from last step - for frontend/delivery
671
477
  cookbook: cookbookDef, // Full cookbook for monitoring trace
672
478
  delivery: delivery, // Delivery configuration from Gateway context (must be object)
673
479
  context: finalContext, // Full context for output resolution
674
- steps: finalContext?.steps || {}, // Steps results (object) for output resolution
480
+ steps: finalContext?.steps || [], // Steps results (array) for output resolution
675
481
  completed_at: new Date().toISOString()
676
482
  };
677
483
 
@@ -699,14 +505,15 @@ class WorkflowOrchestrator {
699
505
  */
700
506
  _getCacheKey(step, context) {
701
507
  const crypto = require('crypto');
508
+ const stepId = this._getStepId(step);
702
509
  const data = JSON.stringify({
703
510
  service: step.service,
704
- operation: step.operation,
511
+ operation: step.operation || stepId,
705
512
  input: step.input,
706
513
  contextInput: context.api_input
707
514
  });
708
515
  const hash = crypto.createHash('sha256').update(data).digest('hex');
709
- return `workflow:step:${step.service}:${step.operation}:${hash}`;
516
+ return `workflow:step:${step.service}:${step.operation || stepId}:${hash}`;
710
517
  }
711
518
 
712
519
  /**
@@ -744,77 +551,6 @@ class WorkflowOrchestrator {
744
551
  const mapper = new ResponseMapper();
745
552
  return mapper.mapResponse(response, mapping);
746
553
  }
747
-
748
- /**
749
- * FAIL-FAST: Validate V2 format strictly
750
- * Rejects V1 format immediately with clear error message
751
- * @private
752
- * @param {Object} cookbook - Cookbook definition
753
- * @throws {Error} If cookbook is not V2 format
754
- */
755
- _validateV2Format(cookbook) {
756
- if (!cookbook) {
757
- throw new Error('FAIL-FAST: Cookbook is required');
758
- }
759
-
760
- // Check version format
761
- const version = cookbook.version;
762
- if (!version) {
763
- throw new Error('FAIL-FAST: Cookbook version is required');
764
- }
765
-
766
- // V1 format detection - REJECT immediately
767
- if (/^1\.\d+\.\d+$/.test(version)) {
768
- throw new Error(`FAIL-FAST: V1 format (${version}) is DEPRECATED and no longer supported. Use V2 format (2.x.x) with step_id instead of id.`);
769
- }
770
-
771
- // Must be V2 format
772
- if (!/^2\.\d+\.\d+$/.test(version)) {
773
- throw new Error(`FAIL-FAST: Invalid version format '${version}'. Must be 2.x.x (e.g., 2.0.0)`);
774
- }
775
-
776
- // Check steps - V2 FORMAT: steps MUST be an OBJECT (keyed by step_id), NOT an array!
777
- if (!cookbook.steps || typeof cookbook.steps !== 'object') {
778
- throw new Error('FAIL-FAST: Cookbook must have steps object');
779
- }
780
-
781
- // V1 format (array) is BANNED!
782
- if (Array.isArray(cookbook.steps)) {
783
- throw new Error('FAIL-FAST: V1 FORMAT BANNED! Cookbook steps must be an OBJECT (keyed by step_id), NOT an array!');
784
- }
785
-
786
- const stepKeys = Object.keys(cookbook.steps);
787
- if (stepKeys.length === 0) {
788
- throw new Error('FAIL-FAST: Cookbook must have at least one step');
789
- }
790
-
791
- // Validate each step has step_id (V2) not id (V1)
792
- const invalidSteps = [];
793
- stepKeys.forEach((stepKey) => {
794
- const step = cookbook.steps[stepKey];
795
- if (step.id && !step.step_id) {
796
- invalidSteps.push(`Step '${stepKey}': has 'id' (V1) but missing 'step_id' (V2)`);
797
- }
798
- if (!step.step_id) {
799
- invalidSteps.push(`Step '${stepKey}': missing required 'step_id'`);
800
- }
801
- if (step.step_id !== stepKey) {
802
- invalidSteps.push(`Step '${stepKey}': step_id '${step.step_id}' does not match key '${stepKey}'`);
803
- }
804
- if (!step.type) {
805
- invalidSteps.push(`Step '${stepKey}': missing required 'type'`);
806
- }
807
- });
808
-
809
- if (invalidSteps.length > 0) {
810
- throw new Error(`FAIL-FAST: Invalid V2 cookbook format:\n${invalidSteps.join('\n')}`);
811
- }
812
-
813
- this.logger.info('[WorkflowOrchestrator] V2 format validation passed', {
814
- version: cookbook.version,
815
- steps: Object.keys(cookbook.steps)
816
- });
817
- }
818
554
  }
819
555
 
820
556
  module.exports = WorkflowOrchestrator;