@onlineapps/cookbook-core 1.0.0 → 2.1.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/README.md CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/cookbook-core",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Core cookbook parsing and validation - lightweight foundation for workflow definitions",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
File without changes
File without changes
File without changes
@@ -0,0 +1,728 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://onlineapps.com/schemas/cookbook/v2.1.0/cookbook.schema.json",
4
+ "title": "Cookbook Workflow Schema v2.1",
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
+ "delivery": {
41
+ "$ref": "#/definitions/DeliveryConfig",
42
+ "description": "Delivery strategy and destinations for workflow output"
43
+ }
44
+ },
45
+ "definitions": {
46
+ "Config": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "properties": {
50
+ "default_timeout_ms": {
51
+ "type": "integer",
52
+ "minimum": 1,
53
+ "description": "Default timeout for all steps in milliseconds"
54
+ },
55
+ "default_retry": {
56
+ "$ref": "#/definitions/RetryConfig",
57
+ "description": "Default retry configuration for all steps"
58
+ }
59
+ }
60
+ },
61
+ "Step": {
62
+ "type": "object",
63
+ "required": ["step_id", "type"],
64
+ "oneOf": [
65
+ { "$ref": "#/definitions/TaskStep" },
66
+ { "$ref": "#/definitions/ForeachStep" },
67
+ { "$ref": "#/definitions/ForkJoinStep" },
68
+ { "$ref": "#/definitions/SwitchStep" },
69
+ { "$ref": "#/definitions/SubWorkflowStep" },
70
+ { "$ref": "#/definitions/WaitStep" },
71
+ { "$ref": "#/definitions/DispatchStep" }
72
+ ]
73
+ },
74
+ "TaskStep": {
75
+ "type": "object",
76
+ "required": ["step_id", "type", "service", "operation"],
77
+ "additionalProperties": false,
78
+ "properties": {
79
+ "step_id": {
80
+ "type": "string",
81
+ "pattern": "^[a-zA-Z0-9_]+$",
82
+ "description": "Unique identifier for the step"
83
+ },
84
+ "type": {
85
+ "const": "task",
86
+ "description": "Step type"
87
+ },
88
+ "name": {
89
+ "type": "string",
90
+ "description": "Human-readable step name"
91
+ },
92
+ "service": {
93
+ "type": "string",
94
+ "pattern": "^[a-z0-9-]+$",
95
+ "description": "Target microservice name"
96
+ },
97
+ "operation": {
98
+ "type": "string",
99
+ "description": "Operation ID from OpenAPI spec (required for task steps)"
100
+ },
101
+ "input": {
102
+ "type": "object",
103
+ "additionalProperties": true,
104
+ "description": "Input parameters for the operation"
105
+ },
106
+ "output": {
107
+ "type": "object",
108
+ "additionalProperties": true,
109
+ "description": "Output mapping to workflow context"
110
+ },
111
+ "timeout_ms": {
112
+ "type": "integer",
113
+ "minimum": 1,
114
+ "description": "Timeout in milliseconds (overrides default)"
115
+ },
116
+ "retry": {
117
+ "$ref": "#/definitions/RetryConfig",
118
+ "description": "Retry configuration (overrides default)"
119
+ },
120
+ "on_error": {
121
+ "$ref": "#/definitions/ErrorHandler",
122
+ "description": "Error handling configuration"
123
+ },
124
+ "compensate": {
125
+ "$ref": "#/definitions/CompensationStep",
126
+ "description": "Rollback/compensation configuration"
127
+ },
128
+ "depends_on": {
129
+ "type": "array",
130
+ "items": {
131
+ "type": "string"
132
+ },
133
+ "description": "Array of step_ids this step depends on"
134
+ }
135
+ }
136
+ },
137
+ "ForeachStep": {
138
+ "type": "object",
139
+ "required": ["step_id", "type", "iterator", "steps"],
140
+ "additionalProperties": false,
141
+ "properties": {
142
+ "step_id": {
143
+ "type": "string",
144
+ "pattern": "^[a-zA-Z0-9_]+$"
145
+ },
146
+ "type": {
147
+ "const": "foreach"
148
+ },
149
+ "name": {
150
+ "type": "string"
151
+ },
152
+ "iterator": {
153
+ "type": "string",
154
+ "pattern": "^\\$\\{.+\\}$",
155
+ "description": "Reference to array to iterate over"
156
+ },
157
+ "steps": {
158
+ "type": "array",
159
+ "minItems": 1,
160
+ "items": {
161
+ "$ref": "#/definitions/Step"
162
+ },
163
+ "description": "Steps to execute for each item"
164
+ },
165
+ "output": {
166
+ "type": "object",
167
+ "additionalProperties": true,
168
+ "description": "How to aggregate results"
169
+ },
170
+ "max_concurrency": {
171
+ "type": "integer",
172
+ "minimum": 1,
173
+ "description": "Maximum parallel executions"
174
+ },
175
+ "on_error": {
176
+ "$ref": "#/definitions/ErrorHandler"
177
+ },
178
+ "depends_on": {
179
+ "type": "array",
180
+ "items": {
181
+ "type": "string"
182
+ }
183
+ }
184
+ }
185
+ },
186
+ "ForkJoinStep": {
187
+ "type": "object",
188
+ "required": ["step_id", "type", "branches", "join"],
189
+ "additionalProperties": false,
190
+ "properties": {
191
+ "step_id": {
192
+ "type": "string",
193
+ "pattern": "^[a-zA-Z0-9_]+$"
194
+ },
195
+ "type": {
196
+ "const": "fork_join"
197
+ },
198
+ "name": {
199
+ "type": "string"
200
+ },
201
+ "branches": {
202
+ "type": "array",
203
+ "minItems": 2,
204
+ "items": {
205
+ "type": "object",
206
+ "required": ["branch_id", "steps"],
207
+ "properties": {
208
+ "branch_id": {
209
+ "type": "string"
210
+ },
211
+ "steps": {
212
+ "type": "array",
213
+ "minItems": 1,
214
+ "items": {
215
+ "$ref": "#/definitions/Step"
216
+ }
217
+ }
218
+ }
219
+ },
220
+ "description": "Parallel branches to execute"
221
+ },
222
+ "join": {
223
+ "type": "object",
224
+ "required": ["strategy"],
225
+ "properties": {
226
+ "strategy": {
227
+ "enum": ["all", "first", "race", "merge"],
228
+ "description": "How to join branch results"
229
+ },
230
+ "timeout_ms": {
231
+ "type": "integer",
232
+ "minimum": 1
233
+ }
234
+ }
235
+ },
236
+ "output": {
237
+ "type": "object",
238
+ "additionalProperties": true
239
+ },
240
+ "on_error": {
241
+ "$ref": "#/definitions/ErrorHandler"
242
+ },
243
+ "depends_on": {
244
+ "type": "array",
245
+ "items": {
246
+ "type": "string"
247
+ }
248
+ }
249
+ }
250
+ },
251
+ "SwitchStep": {
252
+ "type": "object",
253
+ "required": ["step_id", "type", "expression", "cases", "default"],
254
+ "additionalProperties": false,
255
+ "properties": {
256
+ "step_id": {
257
+ "type": "string",
258
+ "pattern": "^[a-zA-Z0-9_]+$"
259
+ },
260
+ "type": {
261
+ "const": "switch"
262
+ },
263
+ "name": {
264
+ "type": "string"
265
+ },
266
+ "expression": {
267
+ "type": "string",
268
+ "pattern": "^\\$\\{.+\\}$",
269
+ "description": "Expression to evaluate for switch"
270
+ },
271
+ "cases": {
272
+ "type": "object",
273
+ "additionalProperties": {
274
+ "type": "object",
275
+ "required": ["steps"],
276
+ "properties": {
277
+ "steps": {
278
+ "type": "array",
279
+ "minItems": 1,
280
+ "items": {
281
+ "$ref": "#/definitions/Step"
282
+ }
283
+ }
284
+ }
285
+ },
286
+ "description": "Case branches"
287
+ },
288
+ "default": {
289
+ "type": "object",
290
+ "required": ["steps"],
291
+ "properties": {
292
+ "steps": {
293
+ "type": "array",
294
+ "minItems": 1,
295
+ "items": {
296
+ "$ref": "#/definitions/Step"
297
+ }
298
+ }
299
+ },
300
+ "description": "Default branch (required)"
301
+ },
302
+ "on_error": {
303
+ "$ref": "#/definitions/ErrorHandler"
304
+ },
305
+ "depends_on": {
306
+ "type": "array",
307
+ "items": {
308
+ "type": "string"
309
+ }
310
+ }
311
+ }
312
+ },
313
+ "SubWorkflowStep": {
314
+ "type": "object",
315
+ "required": ["step_id", "type", "workflow_id"],
316
+ "additionalProperties": false,
317
+ "properties": {
318
+ "step_id": {
319
+ "type": "string",
320
+ "pattern": "^[a-zA-Z0-9_]+$"
321
+ },
322
+ "type": {
323
+ "const": "sub_workflow"
324
+ },
325
+ "name": {
326
+ "type": "string"
327
+ },
328
+ "workflow_id": {
329
+ "type": "string",
330
+ "description": "ID of workflow to execute"
331
+ },
332
+ "parameters": {
333
+ "type": "object",
334
+ "additionalProperties": true,
335
+ "description": "Parameters to pass to sub-workflow"
336
+ },
337
+ "output": {
338
+ "type": "object",
339
+ "additionalProperties": true
340
+ },
341
+ "timeout_ms": {
342
+ "type": "integer",
343
+ "minimum": 1
344
+ },
345
+ "on_error": {
346
+ "$ref": "#/definitions/ErrorHandler"
347
+ },
348
+ "depends_on": {
349
+ "type": "array",
350
+ "items": {
351
+ "type": "string"
352
+ }
353
+ }
354
+ }
355
+ },
356
+ "WaitStep": {
357
+ "type": "object",
358
+ "required": ["step_id", "type", "duration_ms"],
359
+ "additionalProperties": false,
360
+ "properties": {
361
+ "step_id": {
362
+ "type": "string",
363
+ "pattern": "^[a-zA-Z0-9_]+$"
364
+ },
365
+ "type": {
366
+ "const": "wait"
367
+ },
368
+ "name": {
369
+ "type": "string"
370
+ },
371
+ "duration_ms": {
372
+ "type": "integer",
373
+ "minimum": 1,
374
+ "description": "Wait duration in milliseconds"
375
+ },
376
+ "message": {
377
+ "type": "string",
378
+ "description": "Optional message explaining the wait"
379
+ },
380
+ "depends_on": {
381
+ "type": "array",
382
+ "items": {
383
+ "type": "string"
384
+ }
385
+ }
386
+ }
387
+ },
388
+ "DispatchStep": {
389
+ "type": "object",
390
+ "required": ["step_id", "type", "method", "target"],
391
+ "additionalProperties": false,
392
+ "properties": {
393
+ "step_id": {
394
+ "type": "string",
395
+ "pattern": "^[a-zA-Z0-9_]+$"
396
+ },
397
+ "type": {
398
+ "const": "dispatch"
399
+ },
400
+ "name": {
401
+ "type": "string"
402
+ },
403
+ "method": {
404
+ "enum": ["webhook", "http", "grpc", "kafka", "sqs"],
405
+ "description": "Dispatch method"
406
+ },
407
+ "target": {
408
+ "type": "string",
409
+ "description": "Target URL or address"
410
+ },
411
+ "input": {
412
+ "type": "object",
413
+ "additionalProperties": true
414
+ },
415
+ "headers": {
416
+ "type": "object",
417
+ "additionalProperties": {
418
+ "type": "string"
419
+ }
420
+ },
421
+ "timeout_ms": {
422
+ "type": "integer",
423
+ "minimum": 1
424
+ },
425
+ "retry": {
426
+ "$ref": "#/definitions/RetryConfig"
427
+ },
428
+ "on_error": {
429
+ "$ref": "#/definitions/ErrorHandler"
430
+ },
431
+ "depends_on": {
432
+ "type": "array",
433
+ "items": {
434
+ "type": "string"
435
+ }
436
+ }
437
+ }
438
+ },
439
+ "RetryConfig": {
440
+ "type": "object",
441
+ "additionalProperties": false,
442
+ "properties": {
443
+ "max_attempts": {
444
+ "type": "integer",
445
+ "minimum": 1,
446
+ "description": "Maximum retry attempts"
447
+ },
448
+ "delay_ms": {
449
+ "type": "integer",
450
+ "minimum": 0,
451
+ "description": "Delay between retries in milliseconds"
452
+ },
453
+ "backoff": {
454
+ "enum": ["linear", "exponential"],
455
+ "description": "Backoff strategy"
456
+ },
457
+ "max_delay_ms": {
458
+ "type": "integer",
459
+ "minimum": 1,
460
+ "description": "Maximum delay for exponential backoff"
461
+ }
462
+ }
463
+ },
464
+ "ErrorHandler": {
465
+ "type": "object",
466
+ "additionalProperties": false,
467
+ "properties": {
468
+ "strategy": {
469
+ "enum": ["retry", "fail", "continue", "compensate"],
470
+ "description": "Error handling strategy"
471
+ },
472
+ "retry": {
473
+ "$ref": "#/definitions/RetryConfig"
474
+ },
475
+ "catch": {
476
+ "type": "array",
477
+ "items": {
478
+ "type": "object",
479
+ "required": ["error_type", "handler_step"],
480
+ "properties": {
481
+ "error_type": {
482
+ "type": "string",
483
+ "description": "Type of error to catch"
484
+ },
485
+ "handler_step": {
486
+ "type": "string",
487
+ "description": "Step to execute on this error"
488
+ }
489
+ }
490
+ },
491
+ "description": "Specific error handlers"
492
+ },
493
+ "finally": {
494
+ "type": "string",
495
+ "description": "Step to always execute"
496
+ }
497
+ }
498
+ },
499
+ "CompensationStep": {
500
+ "type": "object",
501
+ "required": ["step_id", "service", "operation"],
502
+ "additionalProperties": false,
503
+ "properties": {
504
+ "step_id": {
505
+ "type": "string",
506
+ "pattern": "^[a-zA-Z0-9_]+$",
507
+ "description": "ID for compensation step"
508
+ },
509
+ "service": {
510
+ "type": "string",
511
+ "pattern": "^[a-z0-9-]+$"
512
+ },
513
+ "operation": {
514
+ "type": "string"
515
+ },
516
+ "input": {
517
+ "type": "object",
518
+ "additionalProperties": true
519
+ }
520
+ }
521
+ },
522
+ "DeliveryConfig": {
523
+ "type": "object",
524
+ "required": ["handler"],
525
+ "additionalProperties": false,
526
+ "properties": {
527
+ "handler": {
528
+ "enum": ["dispatcher", "service_step", "none"],
529
+ "description": "Delivery handler selection"
530
+ },
531
+ "allow_skip": {
532
+ "type": "boolean",
533
+ "default": false,
534
+ "description": "Allow dispatcher to skip delivery without failing workflow"
535
+ },
536
+ "delivery_step": {
537
+ "type": "string",
538
+ "pattern": "^[a-zA-Z0-9_]+$",
539
+ "description": "Step ID that performs delivery when handler=service_step"
540
+ },
541
+ "destinations": {
542
+ "type": "array",
543
+ "items": {
544
+ "$ref": "#/definitions/DeliveryDestination"
545
+ },
546
+ "default": []
547
+ },
548
+ "output": {
549
+ "$ref": "#/definitions/DeliveryOutput"
550
+ }
551
+ },
552
+ "allOf": [
553
+ {
554
+ "if": {
555
+ "properties": {
556
+ "handler": { "const": "dispatcher" }
557
+ }
558
+ },
559
+ "then": {
560
+ "required": ["destinations", "output"],
561
+ "properties": {
562
+ "destinations": {
563
+ "minItems": 1
564
+ }
565
+ }
566
+ }
567
+ },
568
+ {
569
+ "if": {
570
+ "properties": {
571
+ "handler": { "const": "service_step" }
572
+ }
573
+ },
574
+ "then": {
575
+ "required": ["delivery_step", "output"]
576
+ }
577
+ }
578
+ ]
579
+ },
580
+ "DeliveryDestination": {
581
+ "type": "object",
582
+ "required": ["type"],
583
+ "properties": {
584
+ "type": {
585
+ "enum": ["webhook", "websocket", "public_url"],
586
+ "description": "Delivery channel type"
587
+ },
588
+ "name": {
589
+ "type": "string",
590
+ "pattern": "^[a-zA-Z0-9._-]+$",
591
+ "description": "Optional identifier for monitoring"
592
+ },
593
+ "enabled": {
594
+ "type": "boolean",
595
+ "default": true
596
+ },
597
+ "retry": {
598
+ "$ref": "#/definitions/RetryConfig"
599
+ }
600
+ },
601
+ "allOf": [
602
+ {
603
+ "if": {
604
+ "properties": {
605
+ "type": { "const": "webhook" }
606
+ }
607
+ },
608
+ "then": {
609
+ "$ref": "#/definitions/DeliveryWebhookOptions"
610
+ }
611
+ },
612
+ {
613
+ "if": {
614
+ "properties": {
615
+ "type": { "const": "websocket" }
616
+ }
617
+ },
618
+ "then": {
619
+ "$ref": "#/definitions/DeliveryWebsocketOptions"
620
+ }
621
+ },
622
+ {
623
+ "if": {
624
+ "properties": {
625
+ "type": { "const": "public_url" }
626
+ }
627
+ },
628
+ "then": {
629
+ "$ref": "#/definitions/DeliveryPublicUrlOptions"
630
+ }
631
+ }
632
+ ]
633
+ },
634
+ "DeliveryWebhookOptions": {
635
+ "type": "object",
636
+ "required": ["url", "method"],
637
+ "properties": {
638
+ "url": {
639
+ "type": "string",
640
+ "format": "uri",
641
+ "description": "Target webhook URL"
642
+ },
643
+ "method": {
644
+ "type": "string",
645
+ "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"]
646
+ },
647
+ "headers": {
648
+ "type": "object",
649
+ "additionalProperties": {
650
+ "type": "string"
651
+ }
652
+ },
653
+ "body_template": {
654
+ "type": ["object", "array"],
655
+ "description": "Optional custom payload template"
656
+ },
657
+ "timeout_ms": {
658
+ "type": "integer",
659
+ "minimum": 100,
660
+ "description": "Request timeout"
661
+ }
662
+ }
663
+ },
664
+ "DeliveryWebsocketOptions": {
665
+ "type": "object",
666
+ "properties": {
667
+ "client_id": {
668
+ "type": "string",
669
+ "description": "Client session identifier"
670
+ },
671
+ "tenant_id": {
672
+ "type": "string",
673
+ "description": "Tenant/topic identifier"
674
+ },
675
+ "events": {
676
+ "type": "array",
677
+ "items": {
678
+ "type": "string"
679
+ }
680
+ }
681
+ },
682
+ "anyOf": [
683
+ { "required": ["client_id"] },
684
+ { "required": ["tenant_id"] }
685
+ ]
686
+ },
687
+ "DeliveryPublicUrlOptions": {
688
+ "type": "object",
689
+ "required": ["path"],
690
+ "properties": {
691
+ "path": {
692
+ "type": "string",
693
+ "pattern": "^[a-zA-Z0-9._\\-/\\${}]+$",
694
+ "description": "Storage key / relative path"
695
+ },
696
+ "ttl_seconds": {
697
+ "type": "integer",
698
+ "minimum": 60,
699
+ "description": "Time to live for generated URL"
700
+ },
701
+ "access": {
702
+ "type": "string",
703
+ "enum": ["public", "signed"],
704
+ "default": "signed"
705
+ },
706
+ "max_downloads": {
707
+ "type": "integer",
708
+ "minimum": 1
709
+ }
710
+ }
711
+ },
712
+ "DeliveryOutput": {
713
+ "type": "object",
714
+ "propertyNames": {
715
+ "pattern": "^[a-zA-Z0-9_]+$"
716
+ },
717
+ "additionalProperties": {
718
+ "type": [
719
+ "string",
720
+ "number",
721
+ "boolean",
722
+ "object",
723
+ "array"
724
+ ]
725
+ }
726
+ }
727
+ }
728
+ }
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.1.0'
60
64
  };
File without changes
@@ -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,339 @@
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
+ const retry = step.retry || {};
160
+ if ('maxAttempts' in retry) {
161
+ suggestions.push(`- Step ${idx}: Rename 'retry.maxAttempts' to 'retry.max_attempts'`);
162
+ }
163
+ if ('delayMs' in retry) {
164
+ suggestions.push(`- Step ${idx}: Rename 'retry.delayMs' to 'retry.delay_ms'`);
165
+ }
166
+ });
167
+ }
168
+
169
+ if (suggestions.length > 0) {
170
+ console.warn('\n⚠️ Migration suggestions for v2.0:');
171
+ console.warn(suggestions.join('\n'));
172
+ console.warn('\nRun migration script: npm run migrate:cookbook v1-to-v2 <input> <output>\n');
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Main validation function
179
+ *
180
+ * @param {object} cookbook - The cookbook object to validate
181
+ * @param {object} options - Validation options
182
+ * @param {string} options.version - Force specific version validation
183
+ * @param {boolean} options.strict - Use strict validation (default: true)
184
+ * @param {boolean} options.warnOnly - Only warn, don't throw (default: false)
185
+ * @returns {boolean} True if valid, throws error otherwise
186
+ */
187
+ function validateCookbook(cookbook, options = {}) {
188
+ const {
189
+ version = null,
190
+ strict = true,
191
+ warnOnly = false
192
+ } = options;
193
+
194
+ // Detect version if not specified
195
+ const cookbookVersion = version || detectVersion(cookbook);
196
+
197
+ // Check if migration needed
198
+ checkMigration(cookbook, cookbookVersion);
199
+
200
+ // Load appropriate schema
201
+ const schema = loadSchema(cookbookVersion);
202
+
203
+ // Create validator
204
+ const validate = createValidator(schema);
205
+
206
+ // Validate
207
+ const isValid = validate(cookbook);
208
+
209
+ if (!isValid) {
210
+ const errorMessage = formatErrors(validate.errors);
211
+
212
+ if (warnOnly) {
213
+ console.warn('⚠️ Cookbook validation warnings:\n' + errorMessage);
214
+ return false;
215
+ } else {
216
+ throw new CookbookValidationError(
217
+ 'Cookbook validation failed:\n' + errorMessage,
218
+ validate.errors
219
+ );
220
+ }
221
+ }
222
+
223
+ // Additional semantic validation (still using schema as source of truth)
224
+ validateSemantics(cookbook, cookbookVersion);
225
+
226
+ return true;
227
+ }
228
+
229
+ /**
230
+ * Semantic validation for things JSON Schema can't easily express
231
+ */
232
+ function validateSemantics(cookbook, version) {
233
+ const stepIds = new Set();
234
+
235
+ // Check for duplicate step_ids (or ids in v1)
236
+ const idField = version === '1.0' ? 'id' : 'step_id';
237
+
238
+ function checkStep(step, path) {
239
+ const stepId = step[idField];
240
+ if (stepId) {
241
+ if (stepIds.has(stepId)) {
242
+ throw new CookbookValidationError(
243
+ `Duplicate ${idField} '${stepId}' found at ${path}`
244
+ );
245
+ }
246
+ stepIds.add(stepId);
247
+ }
248
+
249
+ // Check nested steps
250
+ if (step.steps) {
251
+ step.steps.forEach((nested, idx) =>
252
+ checkStep(nested, `${path}.steps[${idx}]`)
253
+ );
254
+ }
255
+ if (step.body) {
256
+ step.body.forEach((nested, idx) =>
257
+ checkStep(nested, `${path}.body[${idx}]`)
258
+ );
259
+ }
260
+ if (step.branches) {
261
+ step.branches.forEach((branch, idx) => {
262
+ if (branch.steps) {
263
+ branch.steps.forEach((nested, jdx) =>
264
+ checkStep(nested, `${path}.branches[${idx}].steps[${jdx}]`)
265
+ );
266
+ }
267
+ });
268
+ }
269
+ if (step.cases) {
270
+ Object.keys(step.cases).forEach(caseKey => {
271
+ const caseVal = step.cases[caseKey];
272
+ if (caseVal.steps) {
273
+ caseVal.steps.forEach((nested, idx) =>
274
+ checkStep(nested, `${path}.cases.${caseKey}.steps[${idx}]`)
275
+ );
276
+ }
277
+ });
278
+ }
279
+ }
280
+
281
+ // Check all steps
282
+ if (cookbook.steps) {
283
+ cookbook.steps.forEach((step, idx) =>
284
+ checkStep(step, `steps[${idx}]`)
285
+ );
286
+ }
287
+
288
+ // Check dependencies exist
289
+ if (version === '2.0' && cookbook.steps) {
290
+ cookbook.steps.forEach((step, idx) => {
291
+ if (step.depends_on) {
292
+ step.depends_on.forEach(dep => {
293
+ if (!stepIds.has(dep)) {
294
+ throw new CookbookValidationError(
295
+ `Step ${idx} depends on non-existent step_id '${dep}'`
296
+ );
297
+ }
298
+ });
299
+ }
300
+ });
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Validate a single step (for testing purposes)
306
+ */
307
+ function validateStep(step, version = '2.0') {
308
+ const schema = loadSchema(version);
309
+ const stepSchema = schema.definitions?.Step || schema.definitions?.TaskStep;
310
+
311
+ if (!stepSchema) {
312
+ throw new CookbookValidationError('Step schema definition not found');
313
+ }
314
+
315
+ const validate = createValidator({
316
+ ...stepSchema,
317
+ $schema: schema.$schema
318
+ });
319
+
320
+ const isValid = validate(step);
321
+
322
+ if (!isValid) {
323
+ throw new CookbookValidationError(
324
+ 'Step validation failed:\n' + formatErrors(validate.errors),
325
+ validate.errors
326
+ );
327
+ }
328
+
329
+ return true;
330
+ }
331
+
332
+ // Export functions
333
+ module.exports = {
334
+ validateCookbook,
335
+ validateStep,
336
+ CookbookValidationError,
337
+ detectVersion,
338
+ loadSchema
339
+ };
package/src/parser.js CHANGED
File without changes
File without changes
package/src/schema.js CHANGED
File without changes
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
  };