@onlineapps/conn-orch-orchestrator 1.0.30 → 1.0.32

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.30",
3
+ "version": "1.0.32",
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
- }
304
-
305
- // Debug: log context keys and api_input
306
- console.log(`[WorkflowOrchestrator] [RESOLVE] Step ${step.step_id} - context keys:`, Object.keys(context || {}));
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 }))));
307
284
 
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,39 +314,12 @@ class WorkflowOrchestrator {
389
314
  if (!input) return input;
390
315
 
391
316
  const resolveValue = (value) => {
392
- if (typeof value === 'string') {
393
- let result = value;
394
-
395
- // Replace {{...}} references (V2 preferred syntax)
396
- if (result.includes('{{')) {
397
- result = result.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
398
- const resolved = this._getValueFromPath(path.trim(), context);
399
- if (resolved !== undefined) {
400
- // If resolved value is an object, stringify it for embedding in strings
401
- if (typeof resolved === 'object' && result !== match) {
402
- return JSON.stringify(resolved);
403
- }
404
- return resolved;
405
- }
406
- return match;
407
- });
408
- }
409
-
410
- // Replace ${...} references (legacy syntax)
411
- if (result.includes('${')) {
412
- result = result.replace(/\$\{([^}]+)\}/g, (match, path) => {
413
- const resolved = this._getValueFromPath(path.trim(), context);
414
- if (resolved !== undefined) {
415
- if (typeof resolved === 'object' && result !== match) {
416
- return JSON.stringify(resolved);
417
- }
418
- return resolved;
419
- }
420
- return match;
421
- });
422
- }
423
-
424
- 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
+ });
425
323
  }
426
324
  if (Array.isArray(value)) {
427
325
  return value.map(resolveValue);
@@ -440,34 +338,16 @@ class WorkflowOrchestrator {
440
338
  /**
441
339
  * Get value from dot/bracket notation path
442
340
  * @private
443
- * @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"
444
342
  * @param {Object} context - Context object
445
343
  * @returns {*} Resolved value or undefined
446
344
  */
447
345
  _getValueFromPath(path, context) {
448
- // FAIL-FAST: Verify context exists
449
- if (!context) {
450
- console.error('[WorkflowOrchestrator] [FAIL-FAST] _getValueFromPath called with null/undefined context', { path });
451
- throw new Error(`Cannot resolve path '${path}': context is null or undefined`);
452
- }
453
-
454
- // V2: steps is an object keyed by step_id, not an array
455
- // Support both formats for backward compatibility during migration:
456
- // - steps.step_id.output.field (V2, preferred)
457
- // - steps[0].output.field (legacy, deprecated)
458
-
459
- // Normalize bracket notation to dot notation: steps[0] -> steps.0 (legacy)
460
- // But prefer step_id access: steps.step_id -> steps.step_id (V2)
461
- let normalized = path;
462
-
463
- // Handle legacy array access steps[N] - convert to numeric key
464
- // But only if steps is actually an array (fallback for old data)
465
- if (normalized.includes('[') && normalized.includes(']')) {
466
- normalized = normalized.replace(/\[(\d+)\]/g, '.$1');
467
- }
346
+ // Normalize bracket notation to dot notation: steps[0] -> steps.0
347
+ let normalized = path.replace(/\[(\d+)\]/g, '.$1');
468
348
 
469
349
  // Strip prefixes - we're already searching in context object
470
- // Supports: ${context.X}, ${api_input.X}, ${steps.step_id.X} (V2)
350
+ // Supports: ${context.X}, ${api_input.X}, ${steps[N].X}
471
351
  if (normalized.startsWith('context.')) {
472
352
  normalized = normalized.substring(8); // Remove 'context.'
473
353
  }
@@ -475,46 +355,9 @@ class WorkflowOrchestrator {
475
355
 
476
356
  const parts = normalized.split('.');
477
357
 
478
- // FAIL-FAST: Log api_input access attempts
479
- if (normalized.startsWith('api_input.')) {
480
- console.log(`[WorkflowOrchestrator] [GET_VALUE] Resolving api_input path: ${path} -> ${normalized}`);
481
- console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input exists:`, context.api_input !== undefined);
482
- console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input type:`, typeof context.api_input);
483
- if (context.api_input) {
484
- console.log(`[WorkflowOrchestrator] [GET_VALUE] context.api_input keys:`, Object.keys(context.api_input));
485
- }
486
- }
487
-
488
358
  let current = context;
489
359
  for (const part of parts) {
490
- if (current === undefined || current === null) {
491
- // FAIL-FAST: Log failed resolution for api_input
492
- if (normalized.startsWith('api_input.')) {
493
- console.error(`[WorkflowOrchestrator] [FAIL-FAST] Failed to resolve api_input path: ${path}`, {
494
- path,
495
- normalized,
496
- parts,
497
- failed_at_part: part,
498
- current_type: typeof current,
499
- context_keys: Object.keys(context),
500
- context_has_api_input: 'api_input' in context,
501
- api_input_type: typeof context.api_input,
502
- api_input_keys: context.api_input ? Object.keys(context.api_input) : null
503
- });
504
- throw new Error(`Failed to resolve api_input path '${path}': '${part}' is undefined in context`);
505
- }
506
- return undefined;
507
- }
508
-
509
- // Special handling for steps: if accessing by numeric index and steps is object,
510
- // try to find step by index in cookbook (fallback for legacy)
511
- if (part === 'steps' && parts.length > 1) {
512
- current = current.steps;
513
- // If steps is object (V2), next part is step_id
514
- // If steps is array (legacy), next part is numeric index
515
- continue;
516
- }
517
-
360
+ if (current === undefined || current === null) return undefined;
518
361
  current = current[part];
519
362
  }
520
363
  return current;
@@ -549,7 +392,7 @@ class WorkflowOrchestrator {
549
392
  const result = await executor.executeStep(step);
550
393
 
551
394
  this.logger.info(`Control flow step executed`, {
552
- step: step.step_id,
395
+ step: this._getStepId(step),
553
396
  type: step.type
554
397
  });
555
398
 
@@ -564,9 +407,9 @@ class WorkflowOrchestrator {
564
407
  * @param {Object} cookbookDef - Full cookbook definition
565
408
  * @param {Object} context - Updated context
566
409
  * @param {string} workflow_id - Workflow ID
567
- * @param {string} nextStepId - Next step step_id (V2) or id (V1 fallback)
568
410
  */
569
- async _routeToNextStep(nextStep, cookbookDef, context, workflow_id, nextStepId) {
411
+ async _routeToNextStep(nextStep, cookbookDef, context, workflow_id) {
412
+ const nextStepId = this._getStepId(nextStep);
570
413
  const message = {
571
414
  workflow_id,
572
415
  cookbook: cookbookDef,
@@ -624,21 +467,6 @@ class WorkflowOrchestrator {
624
467
  // Delivery Dispatcher requires: workflow_id, status, delivery (must be object, not null)
625
468
  const delivery = finalContext?.delivery || cookbookDef?.delivery || { handler: 'none' };
626
469
 
627
- // Extract api_output from the last completed step
628
- // steps is an object keyed by step_id in V2
629
- let api_output = null;
630
- if (lastStepId && finalContext?.steps?.[lastStepId]?.output) {
631
- api_output = finalContext.steps[lastStepId].output;
632
- } else if (finalContext?.steps) {
633
- // Fallback: find any step with output
634
- const stepKeys = Object.keys(finalContext.steps);
635
- for (const key of stepKeys) {
636
- if (finalContext.steps[key]?.output) {
637
- api_output = finalContext.steps[key].output;
638
- }
639
- }
640
- }
641
-
642
470
  // Build message in format expected by Delivery Dispatcher
643
471
  const workflowCompletedMessage = {
644
472
  workflow_id,
@@ -646,11 +474,10 @@ class WorkflowOrchestrator {
646
474
  service: serviceName, // Service that completed the workflow (for monitoring)
647
475
  step_id: lastStepId, // Last step ID for monitoring
648
476
  step_index: lastStepIndex, // Last step index for monitoring
649
- api_output: api_output, // Output from last step - for frontend/delivery
650
477
  cookbook: cookbookDef, // Full cookbook for monitoring trace
651
478
  delivery: delivery, // Delivery configuration from Gateway context (must be object)
652
479
  context: finalContext, // Full context for output resolution
653
- steps: finalContext?.steps || {}, // Steps results (object) for output resolution
480
+ steps: finalContext?.steps || [], // Steps results (array) for output resolution
654
481
  completed_at: new Date().toISOString()
655
482
  };
656
483
 
@@ -678,14 +505,15 @@ class WorkflowOrchestrator {
678
505
  */
679
506
  _getCacheKey(step, context) {
680
507
  const crypto = require('crypto');
508
+ const stepId = this._getStepId(step);
681
509
  const data = JSON.stringify({
682
510
  service: step.service,
683
- operation: step.operation,
511
+ operation: step.operation || stepId,
684
512
  input: step.input,
685
513
  contextInput: context.api_input
686
514
  });
687
515
  const hash = crypto.createHash('sha256').update(data).digest('hex');
688
- return `workflow:step:${step.service}:${step.operation}:${hash}`;
516
+ return `workflow:step:${step.service}:${step.operation || stepId}:${hash}`;
689
517
  }
690
518
 
691
519
  /**
@@ -723,77 +551,6 @@ class WorkflowOrchestrator {
723
551
  const mapper = new ResponseMapper();
724
552
  return mapper.mapResponse(response, mapping);
725
553
  }
726
-
727
- /**
728
- * FAIL-FAST: Validate V2 format strictly
729
- * Rejects V1 format immediately with clear error message
730
- * @private
731
- * @param {Object} cookbook - Cookbook definition
732
- * @throws {Error} If cookbook is not V2 format
733
- */
734
- _validateV2Format(cookbook) {
735
- if (!cookbook) {
736
- throw new Error('FAIL-FAST: Cookbook is required');
737
- }
738
-
739
- // Check version format
740
- const version = cookbook.version;
741
- if (!version) {
742
- throw new Error('FAIL-FAST: Cookbook version is required');
743
- }
744
-
745
- // V1 format detection - REJECT immediately
746
- if (/^1\.\d+\.\d+$/.test(version)) {
747
- 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.`);
748
- }
749
-
750
- // Must be V2 format
751
- if (!/^2\.\d+\.\d+$/.test(version)) {
752
- throw new Error(`FAIL-FAST: Invalid version format '${version}'. Must be 2.x.x (e.g., 2.0.0)`);
753
- }
754
-
755
- // Check steps - V2 FORMAT: steps MUST be an OBJECT (keyed by step_id), NOT an array!
756
- if (!cookbook.steps || typeof cookbook.steps !== 'object') {
757
- throw new Error('FAIL-FAST: Cookbook must have steps object');
758
- }
759
-
760
- // V1 format (array) is BANNED!
761
- if (Array.isArray(cookbook.steps)) {
762
- throw new Error('FAIL-FAST: V1 FORMAT BANNED! Cookbook steps must be an OBJECT (keyed by step_id), NOT an array!');
763
- }
764
-
765
- const stepKeys = Object.keys(cookbook.steps);
766
- if (stepKeys.length === 0) {
767
- throw new Error('FAIL-FAST: Cookbook must have at least one step');
768
- }
769
-
770
- // Validate each step has step_id (V2) not id (V1)
771
- const invalidSteps = [];
772
- stepKeys.forEach((stepKey) => {
773
- const step = cookbook.steps[stepKey];
774
- if (step.id && !step.step_id) {
775
- invalidSteps.push(`Step '${stepKey}': has 'id' (V1) but missing 'step_id' (V2)`);
776
- }
777
- if (!step.step_id) {
778
- invalidSteps.push(`Step '${stepKey}': missing required 'step_id'`);
779
- }
780
- if (step.step_id !== stepKey) {
781
- invalidSteps.push(`Step '${stepKey}': step_id '${step.step_id}' does not match key '${stepKey}'`);
782
- }
783
- if (!step.type) {
784
- invalidSteps.push(`Step '${stepKey}': missing required 'type'`);
785
- }
786
- });
787
-
788
- if (invalidSteps.length > 0) {
789
- throw new Error(`FAIL-FAST: Invalid V2 cookbook format:\n${invalidSteps.join('\n')}`);
790
- }
791
-
792
- this.logger.info('[WorkflowOrchestrator] V2 format validation passed', {
793
- version: cookbook.version,
794
- steps: Object.keys(cookbook.steps)
795
- });
796
- }
797
554
  }
798
555
 
799
556
  module.exports = WorkflowOrchestrator;