@onlineapps/cookbook-core 1.0.0 → 2.0.0

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/cookbook-core",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Core cookbook parsing and validation - lightweight foundation for workflow definitions",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -0,0 +1,519 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://onlineapps.com/schemas/cookbook/v2.0.0/cookbook.schema.json",
4
+ "title": "Cookbook Workflow Schema v2.0",
5
+ "description": "Schema for defining workflow orchestration with snake_case convention",
6
+ "type": "object",
7
+ "required": ["version", "steps"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "version": {
11
+ "type": "string",
12
+ "pattern": "^2\\.\\d+\\.\\d+$",
13
+ "description": "Schema version (must start with 2.x.x)"
14
+ },
15
+ "name": {
16
+ "type": "string",
17
+ "description": "Human-readable workflow name"
18
+ },
19
+ "description": {
20
+ "type": "string",
21
+ "description": "Workflow description"
22
+ },
23
+ "config": {
24
+ "$ref": "#/definitions/Config",
25
+ "description": "Global configuration and defaults"
26
+ },
27
+ "api_input": {
28
+ "type": "object",
29
+ "description": "Expected input parameters for the workflow",
30
+ "additionalProperties": true
31
+ },
32
+ "steps": {
33
+ "type": "array",
34
+ "minItems": 1,
35
+ "items": {
36
+ "$ref": "#/definitions/Step"
37
+ },
38
+ "description": "Array of workflow steps"
39
+ }
40
+ },
41
+ "definitions": {
42
+ "Config": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "default_timeout_ms": {
47
+ "type": "integer",
48
+ "minimum": 1,
49
+ "description": "Default timeout for all steps in milliseconds"
50
+ },
51
+ "default_retry": {
52
+ "$ref": "#/definitions/RetryConfig",
53
+ "description": "Default retry configuration for all steps"
54
+ }
55
+ }
56
+ },
57
+ "Step": {
58
+ "type": "object",
59
+ "required": ["step_id", "type"],
60
+ "oneOf": [
61
+ { "$ref": "#/definitions/TaskStep" },
62
+ { "$ref": "#/definitions/ForeachStep" },
63
+ { "$ref": "#/definitions/ForkJoinStep" },
64
+ { "$ref": "#/definitions/SwitchStep" },
65
+ { "$ref": "#/definitions/SubWorkflowStep" },
66
+ { "$ref": "#/definitions/WaitStep" },
67
+ { "$ref": "#/definitions/DispatchStep" }
68
+ ]
69
+ },
70
+ "TaskStep": {
71
+ "type": "object",
72
+ "required": ["step_id", "type", "service", "operation"],
73
+ "additionalProperties": false,
74
+ "properties": {
75
+ "step_id": {
76
+ "type": "string",
77
+ "pattern": "^[a-zA-Z0-9_]+$",
78
+ "description": "Unique identifier for the step"
79
+ },
80
+ "type": {
81
+ "const": "task",
82
+ "description": "Step type"
83
+ },
84
+ "name": {
85
+ "type": "string",
86
+ "description": "Human-readable step name"
87
+ },
88
+ "service": {
89
+ "type": "string",
90
+ "pattern": "^[a-z0-9-]+$",
91
+ "description": "Target microservice name"
92
+ },
93
+ "operation": {
94
+ "type": "string",
95
+ "description": "Operation ID from OpenAPI spec (required for task steps)"
96
+ },
97
+ "input": {
98
+ "type": "object",
99
+ "additionalProperties": true,
100
+ "description": "Input parameters for the operation"
101
+ },
102
+ "output": {
103
+ "type": "object",
104
+ "additionalProperties": true,
105
+ "description": "Output mapping to workflow context"
106
+ },
107
+ "timeout_ms": {
108
+ "type": "integer",
109
+ "minimum": 1,
110
+ "description": "Timeout in milliseconds (overrides default)"
111
+ },
112
+ "retry": {
113
+ "$ref": "#/definitions/RetryConfig",
114
+ "description": "Retry configuration (overrides default)"
115
+ },
116
+ "on_error": {
117
+ "$ref": "#/definitions/ErrorHandler",
118
+ "description": "Error handling configuration"
119
+ },
120
+ "compensate": {
121
+ "$ref": "#/definitions/CompensationStep",
122
+ "description": "Rollback/compensation configuration"
123
+ },
124
+ "depends_on": {
125
+ "type": "array",
126
+ "items": {
127
+ "type": "string"
128
+ },
129
+ "description": "Array of step_ids this step depends on"
130
+ }
131
+ }
132
+ },
133
+ "ForeachStep": {
134
+ "type": "object",
135
+ "required": ["step_id", "type", "iterator", "steps"],
136
+ "additionalProperties": false,
137
+ "properties": {
138
+ "step_id": {
139
+ "type": "string",
140
+ "pattern": "^[a-zA-Z0-9_]+$"
141
+ },
142
+ "type": {
143
+ "const": "foreach"
144
+ },
145
+ "name": {
146
+ "type": "string"
147
+ },
148
+ "iterator": {
149
+ "type": "string",
150
+ "pattern": "^\\$\\{.+\\}$",
151
+ "description": "Reference to array to iterate over"
152
+ },
153
+ "steps": {
154
+ "type": "array",
155
+ "minItems": 1,
156
+ "items": {
157
+ "$ref": "#/definitions/Step"
158
+ },
159
+ "description": "Steps to execute for each item"
160
+ },
161
+ "output": {
162
+ "type": "object",
163
+ "additionalProperties": true,
164
+ "description": "How to aggregate results"
165
+ },
166
+ "max_concurrency": {
167
+ "type": "integer",
168
+ "minimum": 1,
169
+ "description": "Maximum parallel executions"
170
+ },
171
+ "on_error": {
172
+ "$ref": "#/definitions/ErrorHandler"
173
+ },
174
+ "depends_on": {
175
+ "type": "array",
176
+ "items": {
177
+ "type": "string"
178
+ }
179
+ }
180
+ }
181
+ },
182
+ "ForkJoinStep": {
183
+ "type": "object",
184
+ "required": ["step_id", "type", "branches", "join"],
185
+ "additionalProperties": false,
186
+ "properties": {
187
+ "step_id": {
188
+ "type": "string",
189
+ "pattern": "^[a-zA-Z0-9_]+$"
190
+ },
191
+ "type": {
192
+ "const": "fork_join"
193
+ },
194
+ "name": {
195
+ "type": "string"
196
+ },
197
+ "branches": {
198
+ "type": "array",
199
+ "minItems": 2,
200
+ "items": {
201
+ "type": "object",
202
+ "required": ["branch_id", "steps"],
203
+ "properties": {
204
+ "branch_id": {
205
+ "type": "string"
206
+ },
207
+ "steps": {
208
+ "type": "array",
209
+ "minItems": 1,
210
+ "items": {
211
+ "$ref": "#/definitions/Step"
212
+ }
213
+ }
214
+ }
215
+ },
216
+ "description": "Parallel branches to execute"
217
+ },
218
+ "join": {
219
+ "type": "object",
220
+ "required": ["strategy"],
221
+ "properties": {
222
+ "strategy": {
223
+ "enum": ["all", "first", "race", "merge"],
224
+ "description": "How to join branch results"
225
+ },
226
+ "timeout_ms": {
227
+ "type": "integer",
228
+ "minimum": 1
229
+ }
230
+ }
231
+ },
232
+ "output": {
233
+ "type": "object",
234
+ "additionalProperties": true
235
+ },
236
+ "on_error": {
237
+ "$ref": "#/definitions/ErrorHandler"
238
+ },
239
+ "depends_on": {
240
+ "type": "array",
241
+ "items": {
242
+ "type": "string"
243
+ }
244
+ }
245
+ }
246
+ },
247
+ "SwitchStep": {
248
+ "type": "object",
249
+ "required": ["step_id", "type", "expression", "cases", "default"],
250
+ "additionalProperties": false,
251
+ "properties": {
252
+ "step_id": {
253
+ "type": "string",
254
+ "pattern": "^[a-zA-Z0-9_]+$"
255
+ },
256
+ "type": {
257
+ "const": "switch"
258
+ },
259
+ "name": {
260
+ "type": "string"
261
+ },
262
+ "expression": {
263
+ "type": "string",
264
+ "pattern": "^\\$\\{.+\\}$",
265
+ "description": "Expression to evaluate for switch"
266
+ },
267
+ "cases": {
268
+ "type": "object",
269
+ "additionalProperties": {
270
+ "type": "object",
271
+ "required": ["steps"],
272
+ "properties": {
273
+ "steps": {
274
+ "type": "array",
275
+ "minItems": 1,
276
+ "items": {
277
+ "$ref": "#/definitions/Step"
278
+ }
279
+ }
280
+ }
281
+ },
282
+ "description": "Case branches"
283
+ },
284
+ "default": {
285
+ "type": "object",
286
+ "required": ["steps"],
287
+ "properties": {
288
+ "steps": {
289
+ "type": "array",
290
+ "minItems": 1,
291
+ "items": {
292
+ "$ref": "#/definitions/Step"
293
+ }
294
+ }
295
+ },
296
+ "description": "Default branch (required)"
297
+ },
298
+ "on_error": {
299
+ "$ref": "#/definitions/ErrorHandler"
300
+ },
301
+ "depends_on": {
302
+ "type": "array",
303
+ "items": {
304
+ "type": "string"
305
+ }
306
+ }
307
+ }
308
+ },
309
+ "SubWorkflowStep": {
310
+ "type": "object",
311
+ "required": ["step_id", "type", "workflow_id"],
312
+ "additionalProperties": false,
313
+ "properties": {
314
+ "step_id": {
315
+ "type": "string",
316
+ "pattern": "^[a-zA-Z0-9_]+$"
317
+ },
318
+ "type": {
319
+ "const": "sub_workflow"
320
+ },
321
+ "name": {
322
+ "type": "string"
323
+ },
324
+ "workflow_id": {
325
+ "type": "string",
326
+ "description": "ID of workflow to execute"
327
+ },
328
+ "parameters": {
329
+ "type": "object",
330
+ "additionalProperties": true,
331
+ "description": "Parameters to pass to sub-workflow"
332
+ },
333
+ "output": {
334
+ "type": "object",
335
+ "additionalProperties": true
336
+ },
337
+ "timeout_ms": {
338
+ "type": "integer",
339
+ "minimum": 1
340
+ },
341
+ "on_error": {
342
+ "$ref": "#/definitions/ErrorHandler"
343
+ },
344
+ "depends_on": {
345
+ "type": "array",
346
+ "items": {
347
+ "type": "string"
348
+ }
349
+ }
350
+ }
351
+ },
352
+ "WaitStep": {
353
+ "type": "object",
354
+ "required": ["step_id", "type", "duration_ms"],
355
+ "additionalProperties": false,
356
+ "properties": {
357
+ "step_id": {
358
+ "type": "string",
359
+ "pattern": "^[a-zA-Z0-9_]+$"
360
+ },
361
+ "type": {
362
+ "const": "wait"
363
+ },
364
+ "name": {
365
+ "type": "string"
366
+ },
367
+ "duration_ms": {
368
+ "type": "integer",
369
+ "minimum": 1,
370
+ "description": "Wait duration in milliseconds"
371
+ },
372
+ "message": {
373
+ "type": "string",
374
+ "description": "Optional message explaining the wait"
375
+ },
376
+ "depends_on": {
377
+ "type": "array",
378
+ "items": {
379
+ "type": "string"
380
+ }
381
+ }
382
+ }
383
+ },
384
+ "DispatchStep": {
385
+ "type": "object",
386
+ "required": ["step_id", "type", "method", "target"],
387
+ "additionalProperties": false,
388
+ "properties": {
389
+ "step_id": {
390
+ "type": "string",
391
+ "pattern": "^[a-zA-Z0-9_]+$"
392
+ },
393
+ "type": {
394
+ "const": "dispatch"
395
+ },
396
+ "name": {
397
+ "type": "string"
398
+ },
399
+ "method": {
400
+ "enum": ["webhook", "http", "grpc", "kafka", "sqs"],
401
+ "description": "Dispatch method"
402
+ },
403
+ "target": {
404
+ "type": "string",
405
+ "description": "Target URL or address"
406
+ },
407
+ "input": {
408
+ "type": "object",
409
+ "additionalProperties": true
410
+ },
411
+ "headers": {
412
+ "type": "object",
413
+ "additionalProperties": {
414
+ "type": "string"
415
+ }
416
+ },
417
+ "timeout_ms": {
418
+ "type": "integer",
419
+ "minimum": 1
420
+ },
421
+ "retry": {
422
+ "$ref": "#/definitions/RetryConfig"
423
+ },
424
+ "on_error": {
425
+ "$ref": "#/definitions/ErrorHandler"
426
+ },
427
+ "depends_on": {
428
+ "type": "array",
429
+ "items": {
430
+ "type": "string"
431
+ }
432
+ }
433
+ }
434
+ },
435
+ "RetryConfig": {
436
+ "type": "object",
437
+ "additionalProperties": false,
438
+ "properties": {
439
+ "max_attempts": {
440
+ "type": "integer",
441
+ "minimum": 1,
442
+ "description": "Maximum retry attempts"
443
+ },
444
+ "delay_ms": {
445
+ "type": "integer",
446
+ "minimum": 0,
447
+ "description": "Delay between retries in milliseconds"
448
+ },
449
+ "backoff": {
450
+ "enum": ["linear", "exponential"],
451
+ "description": "Backoff strategy"
452
+ },
453
+ "max_delay_ms": {
454
+ "type": "integer",
455
+ "minimum": 1,
456
+ "description": "Maximum delay for exponential backoff"
457
+ }
458
+ }
459
+ },
460
+ "ErrorHandler": {
461
+ "type": "object",
462
+ "additionalProperties": false,
463
+ "properties": {
464
+ "strategy": {
465
+ "enum": ["retry", "fail", "continue", "compensate"],
466
+ "description": "Error handling strategy"
467
+ },
468
+ "retry": {
469
+ "$ref": "#/definitions/RetryConfig"
470
+ },
471
+ "catch": {
472
+ "type": "array",
473
+ "items": {
474
+ "type": "object",
475
+ "required": ["error_type", "handler_step"],
476
+ "properties": {
477
+ "error_type": {
478
+ "type": "string",
479
+ "description": "Type of error to catch"
480
+ },
481
+ "handler_step": {
482
+ "type": "string",
483
+ "description": "Step to execute on this error"
484
+ }
485
+ }
486
+ },
487
+ "description": "Specific error handlers"
488
+ },
489
+ "finally": {
490
+ "type": "string",
491
+ "description": "Step to always execute"
492
+ }
493
+ }
494
+ },
495
+ "CompensationStep": {
496
+ "type": "object",
497
+ "required": ["step_id", "service", "operation"],
498
+ "additionalProperties": false,
499
+ "properties": {
500
+ "step_id": {
501
+ "type": "string",
502
+ "pattern": "^[a-zA-Z0-9_]+$",
503
+ "description": "ID for compensation step"
504
+ },
505
+ "service": {
506
+ "type": "string",
507
+ "pattern": "^[a-z0-9-]+$"
508
+ },
509
+ "operation": {
510
+ "type": "string"
511
+ },
512
+ "input": {
513
+ "type": "object",
514
+ "additionalProperties": true
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }
package/src/index.js CHANGED
@@ -16,7 +16,9 @@ const {
16
16
  const {
17
17
  validateCookbook,
18
18
  validateStep,
19
- CookbookValidationError
19
+ CookbookValidationError,
20
+ detectVersion,
21
+ loadSchema: loadValidatorSchema
20
22
  } = require('./validator');
21
23
 
22
24
  const {
@@ -40,6 +42,7 @@ module.exports = {
40
42
  // Validator functions
41
43
  validateCookbook,
42
44
  validateStep,
45
+ detectVersion,
43
46
 
44
47
  // Reference validation
45
48
  validateAllReferences,
@@ -50,11 +53,12 @@ module.exports = {
50
53
  // Schema access
51
54
  CookbookSchema,
52
55
  loadSchema,
56
+ loadValidatorSchema,
53
57
 
54
58
  // Error classes
55
59
  CookbookValidationError,
56
60
 
57
61
  // Utility exports
58
- VERSION: '1.0.0',
59
- SCHEMA_VERSION: '1.0.0'
62
+ VERSION: '2.0.0',
63
+ SCHEMA_VERSION: '2.0.0'
60
64
  };
@@ -281,7 +281,7 @@ function validateStep(step, context, mode = 'default') {
281
281
  }
282
282
  }
283
283
 
284
- allowedProps = new Set([...commonAllowed, 'name', 'service', 'action', 'endpoint', 'input', 'output', 'depends_on', 'retry', 'timeoutMs']);
284
+ allowedProps = new Set([...commonAllowed, 'name', 'service', 'action', 'endpoint', 'operation', 'input', 'output', 'depends_on', 'retry', 'timeoutMs', 'on_error']);
285
285
  break;
286
286
  }
287
287
  case 'foreach': {
@@ -0,0 +1,338 @@
1
+ /**
2
+ * @fileoverview
3
+ * Cookbook Validator v2.0 - Single Source of Truth through JSON Schema
4
+ *
5
+ * This validator uses ONLY JSON Schema (v2.0) for validation.
6
+ * No duplicate manual validation logic.
7
+ *
8
+ * Features:
9
+ * - Full JSON Schema draft-07 validation via AJV
10
+ * - Support for v1.0 backwards compatibility
11
+ * - Automatic migration suggestions for v1.0 cookbooks
12
+ * - Comprehensive error messages with paths
13
+ */
14
+
15
+ const Ajv = require('ajv');
16
+ const addFormats = require('ajv-formats');
17
+ const path = require('path');
18
+ const fs = require('fs');
19
+
20
+ /**
21
+ * Custom Error class for validation errors
22
+ */
23
+ class CookbookValidationError extends Error {
24
+ constructor(message, errors = null) {
25
+ super(message);
26
+ this.name = 'CookbookValidationError';
27
+ this.validationErrors = errors;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Load schema file with version detection
33
+ */
34
+ function loadSchema(version = '2.0') {
35
+ const schemaMap = {
36
+ '1.0': 'cookbook.schema.json', // Legacy v1.0 schema
37
+ '2.0': 'cookbook.v2.schema.json' // New v2.0 schema with snake_case
38
+ };
39
+
40
+ const filename = schemaMap[version.split('.')[0] + '.0'] || schemaMap['2.0'];
41
+ const schemaPath = path.join(__dirname, '../../schemas', filename);
42
+
43
+ if (!fs.existsSync(schemaPath)) {
44
+ throw new CookbookValidationError(`Schema file not found: ${filename}`);
45
+ }
46
+
47
+ return JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
48
+ }
49
+
50
+ /**
51
+ * Create AJV validator instance with custom error messages
52
+ */
53
+ function createValidator(schema) {
54
+ const ajv = new Ajv({
55
+ allErrors: true,
56
+ strict: false, // Needed for 'condition' property
57
+ verbose: true
58
+ });
59
+
60
+ addFormats(ajv);
61
+
62
+ // Add custom error messages
63
+ ajv.addKeyword({
64
+ keyword: 'errorMessage',
65
+ schemaType: 'string',
66
+ compile: (schemaVal) => {
67
+ return function validate(data, dataPath) {
68
+ validate.errors = [{
69
+ keyword: 'errorMessage',
70
+ message: schemaVal,
71
+ params: {}
72
+ }];
73
+ return true;
74
+ };
75
+ }
76
+ });
77
+
78
+ return ajv.compile(schema);
79
+ }
80
+
81
+ /**
82
+ * Format AJV errors into readable messages
83
+ */
84
+ function formatErrors(errors) {
85
+ if (!errors || errors.length === 0) return 'Unknown validation error';
86
+
87
+ return errors.map(err => {
88
+ const path = err.instancePath || err.dataPath || '/';
89
+ const field = path.split('/').pop() || 'root';
90
+
91
+ switch (err.keyword) {
92
+ case 'required':
93
+ return `Missing required field '${err.params.missingProperty}' at ${path}`;
94
+
95
+ case 'additionalProperties':
96
+ return `Unexpected field '${err.params.additionalProperty}' at ${path}`;
97
+
98
+ case 'enum':
99
+ return `Invalid value for '${field}' at ${path}. Allowed: ${err.params.allowedValues.join(', ')}`;
100
+
101
+ case 'type':
102
+ return `Invalid type for '${field}' at ${path}. Expected ${err.params.type}, got ${typeof err.data}`;
103
+
104
+ case 'pattern':
105
+ return `Invalid format for '${field}' at ${path}. Must match pattern: ${err.params.pattern}`;
106
+
107
+ case 'const':
108
+ return `Field '${field}' at ${path} must be exactly '${err.params.allowedValue}'`;
109
+
110
+ default:
111
+ return err.message || `Validation error at ${path}`;
112
+ }
113
+ }).join('\n');
114
+ }
115
+
116
+ /**
117
+ * Detect cookbook version and suggest migration if needed
118
+ */
119
+ function detectVersion(cookbook) {
120
+ // Check version field first
121
+ if (cookbook.version) {
122
+ const major = cookbook.version.split('.')[0];
123
+ if (major === '1') return '1.0';
124
+ if (major === '2') return '2.0';
125
+ }
126
+
127
+ // Auto-detect based on field names
128
+ const hasV1Fields = cookbook.steps && cookbook.steps.some(step =>
129
+ 'id' in step && !('step_id' in step)
130
+ );
131
+
132
+ const hasV2Fields = cookbook.steps && cookbook.steps.some(step =>
133
+ 'step_id' in step && !('id' in step)
134
+ );
135
+
136
+ if (hasV1Fields) return '1.0';
137
+ if (hasV2Fields) return '2.0';
138
+
139
+ // Default to v2.0 for new cookbooks
140
+ return '2.0';
141
+ }
142
+
143
+ /**
144
+ * Check if migration is needed and provide suggestions
145
+ */
146
+ function checkMigration(cookbook, detectedVersion) {
147
+ if (detectedVersion === '1.0') {
148
+ const suggestions = [];
149
+
150
+ // Check for camelCase fields that should be snake_case
151
+ if (cookbook.steps) {
152
+ cookbook.steps.forEach((step, idx) => {
153
+ if ('id' in step) {
154
+ suggestions.push(`- Step ${idx}: Rename 'id' to 'step_id'`);
155
+ }
156
+ if ('timeoutMs' in step) {
157
+ suggestions.push(`- Step ${idx}: Rename 'timeoutMs' to 'timeout_ms'`);
158
+ }
159
+ if ('maxAttempts' in step.retry || {}) {
160
+ suggestions.push(`- Step ${idx}: Rename 'retry.maxAttempts' to 'retry.max_attempts'`);
161
+ }
162
+ if ('delayMs' in step.retry || {}) {
163
+ suggestions.push(`- Step ${idx}: Rename 'retry.delayMs' to 'retry.delay_ms'`);
164
+ }
165
+ });
166
+ }
167
+
168
+ if (suggestions.length > 0) {
169
+ console.warn('\n⚠️ Migration suggestions for v2.0:');
170
+ console.warn(suggestions.join('\n'));
171
+ console.warn('\nRun migration script: npm run migrate:cookbook v1-to-v2 <input> <output>\n');
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Main validation function
178
+ *
179
+ * @param {object} cookbook - The cookbook object to validate
180
+ * @param {object} options - Validation options
181
+ * @param {string} options.version - Force specific version validation
182
+ * @param {boolean} options.strict - Use strict validation (default: true)
183
+ * @param {boolean} options.warnOnly - Only warn, don't throw (default: false)
184
+ * @returns {boolean} True if valid, throws error otherwise
185
+ */
186
+ function validateCookbook(cookbook, options = {}) {
187
+ const {
188
+ version = null,
189
+ strict = true,
190
+ warnOnly = false
191
+ } = options;
192
+
193
+ // Detect version if not specified
194
+ const cookbookVersion = version || detectVersion(cookbook);
195
+
196
+ // Check if migration needed
197
+ checkMigration(cookbook, cookbookVersion);
198
+
199
+ // Load appropriate schema
200
+ const schema = loadSchema(cookbookVersion);
201
+
202
+ // Create validator
203
+ const validate = createValidator(schema);
204
+
205
+ // Validate
206
+ const isValid = validate(cookbook);
207
+
208
+ if (!isValid) {
209
+ const errorMessage = formatErrors(validate.errors);
210
+
211
+ if (warnOnly) {
212
+ console.warn('⚠️ Cookbook validation warnings:\n' + errorMessage);
213
+ return false;
214
+ } else {
215
+ throw new CookbookValidationError(
216
+ 'Cookbook validation failed:\n' + errorMessage,
217
+ validate.errors
218
+ );
219
+ }
220
+ }
221
+
222
+ // Additional semantic validation (still using schema as source of truth)
223
+ validateSemantics(cookbook, cookbookVersion);
224
+
225
+ return true;
226
+ }
227
+
228
+ /**
229
+ * Semantic validation for things JSON Schema can't easily express
230
+ */
231
+ function validateSemantics(cookbook, version) {
232
+ const stepIds = new Set();
233
+
234
+ // Check for duplicate step_ids (or ids in v1)
235
+ const idField = version === '1.0' ? 'id' : 'step_id';
236
+
237
+ function checkStep(step, path) {
238
+ const stepId = step[idField];
239
+ if (stepId) {
240
+ if (stepIds.has(stepId)) {
241
+ throw new CookbookValidationError(
242
+ `Duplicate ${idField} '${stepId}' found at ${path}`
243
+ );
244
+ }
245
+ stepIds.add(stepId);
246
+ }
247
+
248
+ // Check nested steps
249
+ if (step.steps) {
250
+ step.steps.forEach((nested, idx) =>
251
+ checkStep(nested, `${path}.steps[${idx}]`)
252
+ );
253
+ }
254
+ if (step.body) {
255
+ step.body.forEach((nested, idx) =>
256
+ checkStep(nested, `${path}.body[${idx}]`)
257
+ );
258
+ }
259
+ if (step.branches) {
260
+ step.branches.forEach((branch, idx) => {
261
+ if (branch.steps) {
262
+ branch.steps.forEach((nested, jdx) =>
263
+ checkStep(nested, `${path}.branches[${idx}].steps[${jdx}]`)
264
+ );
265
+ }
266
+ });
267
+ }
268
+ if (step.cases) {
269
+ Object.keys(step.cases).forEach(caseKey => {
270
+ const caseVal = step.cases[caseKey];
271
+ if (caseVal.steps) {
272
+ caseVal.steps.forEach((nested, idx) =>
273
+ checkStep(nested, `${path}.cases.${caseKey}.steps[${idx}]`)
274
+ );
275
+ }
276
+ });
277
+ }
278
+ }
279
+
280
+ // Check all steps
281
+ if (cookbook.steps) {
282
+ cookbook.steps.forEach((step, idx) =>
283
+ checkStep(step, `steps[${idx}]`)
284
+ );
285
+ }
286
+
287
+ // Check dependencies exist
288
+ if (version === '2.0' && cookbook.steps) {
289
+ cookbook.steps.forEach((step, idx) => {
290
+ if (step.depends_on) {
291
+ step.depends_on.forEach(dep => {
292
+ if (!stepIds.has(dep)) {
293
+ throw new CookbookValidationError(
294
+ `Step ${idx} depends on non-existent step_id '${dep}'`
295
+ );
296
+ }
297
+ });
298
+ }
299
+ });
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Validate a single step (for testing purposes)
305
+ */
306
+ function validateStep(step, version = '2.0') {
307
+ const schema = loadSchema(version);
308
+ const stepSchema = schema.definitions?.Step || schema.definitions?.TaskStep;
309
+
310
+ if (!stepSchema) {
311
+ throw new CookbookValidationError('Step schema definition not found');
312
+ }
313
+
314
+ const validate = createValidator({
315
+ ...stepSchema,
316
+ $schema: schema.$schema
317
+ });
318
+
319
+ const isValid = validate(step);
320
+
321
+ if (!isValid) {
322
+ throw new CookbookValidationError(
323
+ 'Step validation failed:\n' + formatErrors(validate.errors),
324
+ validate.errors
325
+ );
326
+ }
327
+
328
+ return true;
329
+ }
330
+
331
+ // Export functions
332
+ module.exports = {
333
+ validateCookbook,
334
+ validateStep,
335
+ CookbookValidationError,
336
+ detectVersion,
337
+ loadSchema
338
+ };
package/src/validator.js CHANGED
@@ -1,17 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Re-export validator functions from the parser module
4
+ * Re-export validator functions from the v2.0 parser module
5
+ * This now uses the v2.0 validator as the primary validator
5
6
  */
6
7
 
7
8
  const {
8
9
  validateCookbook,
9
10
  validateStep,
10
- CookbookValidationError
11
- } = require('./parser/cookbookValidator');
11
+ CookbookValidationError,
12
+ detectVersion,
13
+ loadSchema
14
+ } = require('./parser/cookbookValidatorV2');
12
15
 
13
16
  module.exports = {
14
17
  validateCookbook,
15
18
  validateStep,
16
- CookbookValidationError
19
+ CookbookValidationError,
20
+ detectVersion,
21
+ loadSchema
17
22
  };