@redush/sysconst-validator 1.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/dist/index.js ADDED
@@ -0,0 +1,1195 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ parseSpec: () => parseSpec,
24
+ validate: () => validate,
25
+ validatePhase: () => validatePhase,
26
+ validateYaml: () => validateYaml
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_yaml = require("yaml");
30
+
31
+ // src/phases/01-structural.ts
32
+ var VALID_KINDS = [
33
+ "System",
34
+ "Module",
35
+ "Entity",
36
+ "Enum",
37
+ "Value",
38
+ "Interface",
39
+ "Command",
40
+ "Event",
41
+ "Query",
42
+ "Process",
43
+ "Step",
44
+ "Policy",
45
+ "Scenario",
46
+ "Contract"
47
+ ];
48
+ var ID_PATTERN = /^[a-z][a-z0-9_.-]*$/;
49
+ function validateStructural(spec) {
50
+ const errors = [];
51
+ if (!spec || typeof spec !== "object") {
52
+ errors.push({
53
+ code: "STRUCTURAL_ERROR",
54
+ phase: 1,
55
+ level: "hard",
56
+ message: "Spec must be an object",
57
+ location: ""
58
+ });
59
+ return errors;
60
+ }
61
+ const s = spec;
62
+ if (!("spec" in s)) {
63
+ errors.push({
64
+ code: "MISSING_SPEC_VERSION",
65
+ phase: 1,
66
+ level: "hard",
67
+ message: "Missing 'spec' field",
68
+ location: "",
69
+ suggestion: "Add 'spec: sysconst/v1' at the root"
70
+ });
71
+ } else if (s.spec !== "sysconst/v1") {
72
+ errors.push({
73
+ code: "INVALID_SPEC_VERSION",
74
+ phase: 1,
75
+ level: "hard",
76
+ message: `Invalid spec version: ${s.spec}`,
77
+ location: "spec",
78
+ suggestion: "Use 'spec: sysconst/v1'"
79
+ });
80
+ }
81
+ if (!("project" in s)) {
82
+ errors.push({
83
+ code: "MISSING_PROJECT",
84
+ phase: 1,
85
+ level: "hard",
86
+ message: "Missing 'project' field",
87
+ location: ""
88
+ });
89
+ } else {
90
+ const project = s.project;
91
+ if (!project.id) {
92
+ errors.push({
93
+ code: "MISSING_PROJECT_ID",
94
+ phase: 1,
95
+ level: "hard",
96
+ message: "Missing 'project.id'",
97
+ location: "project"
98
+ });
99
+ }
100
+ if (!project.versioning) {
101
+ errors.push({
102
+ code: "MISSING_VERSIONING",
103
+ phase: 1,
104
+ level: "hard",
105
+ message: "Missing 'project.versioning'",
106
+ location: "project"
107
+ });
108
+ } else {
109
+ const versioning = project.versioning;
110
+ if (!versioning.current) {
111
+ errors.push({
112
+ code: "MISSING_CURRENT_VERSION",
113
+ phase: 1,
114
+ level: "hard",
115
+ message: "Missing 'project.versioning.current'",
116
+ location: "project.versioning"
117
+ });
118
+ }
119
+ }
120
+ }
121
+ if (!("structure" in s)) {
122
+ errors.push({
123
+ code: "MISSING_STRUCTURE",
124
+ phase: 1,
125
+ level: "hard",
126
+ message: "Missing 'structure' field",
127
+ location: ""
128
+ });
129
+ } else {
130
+ const structure = s.structure;
131
+ if (!structure.root) {
132
+ errors.push({
133
+ code: "MISSING_STRUCTURE_ROOT",
134
+ phase: 1,
135
+ level: "hard",
136
+ message: "Missing 'structure.root'",
137
+ location: "structure"
138
+ });
139
+ }
140
+ }
141
+ if (!("domain" in s)) {
142
+ errors.push({
143
+ code: "MISSING_DOMAIN",
144
+ phase: 1,
145
+ level: "hard",
146
+ message: "Missing 'domain' field",
147
+ location: ""
148
+ });
149
+ } else {
150
+ const domain = s.domain;
151
+ if (!domain.nodes || !Array.isArray(domain.nodes)) {
152
+ errors.push({
153
+ code: "MISSING_DOMAIN_NODES",
154
+ phase: 1,
155
+ level: "hard",
156
+ message: "Missing or invalid 'domain.nodes'",
157
+ location: "domain"
158
+ });
159
+ } else {
160
+ const seenIds = /* @__PURE__ */ new Set();
161
+ domain.nodes.forEach((node, index) => {
162
+ const nodeErrors = validateNode(node, `domain.nodes[${index}]`, seenIds);
163
+ errors.push(...nodeErrors);
164
+ });
165
+ }
166
+ }
167
+ return errors;
168
+ }
169
+ function validateNode(node, location, seenIds) {
170
+ const errors = [];
171
+ if (!node || typeof node !== "object") {
172
+ errors.push({
173
+ code: "INVALID_NODE",
174
+ phase: 1,
175
+ level: "hard",
176
+ message: "Node must be an object",
177
+ location
178
+ });
179
+ return errors;
180
+ }
181
+ const n = node;
182
+ if (!("kind" in n)) {
183
+ errors.push({
184
+ code: "MISSING_NODE_KIND",
185
+ phase: 1,
186
+ level: "hard",
187
+ message: "Node missing 'kind'",
188
+ location
189
+ });
190
+ } else if (!VALID_KINDS.includes(n.kind)) {
191
+ errors.push({
192
+ code: "INVALID_NODE_KIND",
193
+ phase: 1,
194
+ level: "hard",
195
+ message: `Invalid node kind: ${n.kind}`,
196
+ location: `${location}.kind`,
197
+ suggestion: `Valid kinds: ${VALID_KINDS.join(", ")}`
198
+ });
199
+ }
200
+ if (!("id" in n)) {
201
+ errors.push({
202
+ code: "MISSING_NODE_ID",
203
+ phase: 1,
204
+ level: "hard",
205
+ message: "Node missing 'id'",
206
+ location
207
+ });
208
+ } else {
209
+ const id = n.id;
210
+ if (!ID_PATTERN.test(id)) {
211
+ errors.push({
212
+ code: "INVALID_NODE_ID",
213
+ phase: 1,
214
+ level: "hard",
215
+ message: `Invalid node ID format: ${id}`,
216
+ location: `${location}.id`,
217
+ suggestion: "ID must match pattern: ^[a-z][a-z0-9_.-]*$"
218
+ });
219
+ }
220
+ if (seenIds.has(id)) {
221
+ errors.push({
222
+ code: "DUPLICATE_NODE_ID",
223
+ phase: 1,
224
+ level: "hard",
225
+ message: `Duplicate node ID: ${id}`,
226
+ location: `${location}.id`
227
+ });
228
+ } else {
229
+ seenIds.add(id);
230
+ }
231
+ }
232
+ if (!("spec" in n)) {
233
+ errors.push({
234
+ code: "MISSING_NODE_SPEC",
235
+ phase: 1,
236
+ level: "hard",
237
+ message: "Node missing 'spec'",
238
+ location
239
+ });
240
+ } else if (typeof n.spec !== "object" || n.spec === null) {
241
+ errors.push({
242
+ code: "MISSING_NODE_SPEC",
243
+ phase: 1,
244
+ level: "hard",
245
+ message: "'spec' must be an object",
246
+ location: `${location}.spec`
247
+ });
248
+ }
249
+ if ("children" in n && n.children !== void 0) {
250
+ if (!Array.isArray(n.children)) {
251
+ errors.push({
252
+ code: "STRUCTURAL_ERROR",
253
+ phase: 1,
254
+ level: "hard",
255
+ message: "'children' must be an array",
256
+ location: `${location}.children`
257
+ });
258
+ }
259
+ }
260
+ return errors;
261
+ }
262
+
263
+ // src/phases/02-referential.ts
264
+ var NODEREF_PATTERN = /^NodeRef\(([a-z][a-z0-9_.-]*)\)$/;
265
+ function validateReferential(spec) {
266
+ const errors = [];
267
+ const nodeIndex = /* @__PURE__ */ new Map();
268
+ for (const node of spec.domain.nodes) {
269
+ nodeIndex.set(node.id, node);
270
+ }
271
+ const rootRef = spec.structure.root;
272
+ const rootMatch = NODEREF_PATTERN.exec(rootRef);
273
+ if (!rootMatch) {
274
+ errors.push({
275
+ code: "UNRESOLVED_ROOT",
276
+ phase: 2,
277
+ level: "hard",
278
+ message: `Invalid root reference format: ${rootRef}`,
279
+ location: "structure.root",
280
+ suggestion: "Use format: NodeRef(system.xxx)"
281
+ });
282
+ } else {
283
+ const rootId = rootMatch[1];
284
+ const rootNode = nodeIndex.get(rootId);
285
+ if (!rootNode) {
286
+ errors.push({
287
+ code: "UNRESOLVED_ROOT",
288
+ phase: 2,
289
+ level: "hard",
290
+ message: `Root node not found: ${rootId}`,
291
+ location: "structure.root"
292
+ });
293
+ } else if (rootNode.kind !== "System") {
294
+ errors.push({
295
+ code: "INVALID_ROOT_KIND",
296
+ phase: 2,
297
+ level: "hard",
298
+ message: `Root node must be System, got: ${rootNode.kind}`,
299
+ location: "structure.root"
300
+ });
301
+ }
302
+ }
303
+ spec.domain.nodes.forEach((node, index) => {
304
+ if (node.children) {
305
+ node.children.forEach((child, childIndex) => {
306
+ if (typeof child === "string") {
307
+ const match = NODEREF_PATTERN.exec(child);
308
+ if (match) {
309
+ const refId = match[1];
310
+ if (!nodeIndex.has(refId)) {
311
+ errors.push({
312
+ code: "UNRESOLVED_NODEREF",
313
+ phase: 2,
314
+ level: "hard",
315
+ message: `NodeRef does not resolve: ${child}`,
316
+ location: `domain.nodes[${index}].children[${childIndex}]`
317
+ });
318
+ }
319
+ } else {
320
+ errors.push({
321
+ code: "UNRESOLVED_NODEREF",
322
+ phase: 2,
323
+ level: "hard",
324
+ message: `Invalid NodeRef format: ${child}`,
325
+ location: `domain.nodes[${index}].children[${childIndex}]`,
326
+ suggestion: "Use format: NodeRef(node.id)"
327
+ });
328
+ }
329
+ }
330
+ });
331
+ }
332
+ });
333
+ const circularErrors = detectCircularChildren(spec.domain.nodes, nodeIndex);
334
+ errors.push(...circularErrors);
335
+ if (spec.tests?.scenarios) {
336
+ spec.tests.scenarios.forEach((ref, index) => {
337
+ const match = NODEREF_PATTERN.exec(ref);
338
+ if (match) {
339
+ const refId = match[1];
340
+ if (!nodeIndex.has(refId)) {
341
+ errors.push({
342
+ code: "UNRESOLVED_NODEREF",
343
+ phase: 2,
344
+ level: "hard",
345
+ message: `Scenario reference does not resolve: ${ref}`,
346
+ location: `tests.scenarios[${index}]`
347
+ });
348
+ }
349
+ }
350
+ });
351
+ }
352
+ return errors;
353
+ }
354
+ function detectCircularChildren(nodes, nodeIndex) {
355
+ const errors = [];
356
+ const visited = /* @__PURE__ */ new Set();
357
+ const recursionStack = /* @__PURE__ */ new Set();
358
+ function dfs(nodeId, path) {
359
+ if (recursionStack.has(nodeId)) {
360
+ errors.push({
361
+ code: "CIRCULAR_CHILDREN",
362
+ phase: 2,
363
+ level: "hard",
364
+ message: `Circular reference detected: ${[...path, nodeId].join(" -> ")}`,
365
+ location: `domain.nodes`,
366
+ context: { cycle: [...path, nodeId] }
367
+ });
368
+ return true;
369
+ }
370
+ if (visited.has(nodeId)) {
371
+ return false;
372
+ }
373
+ visited.add(nodeId);
374
+ recursionStack.add(nodeId);
375
+ const node = nodeIndex.get(nodeId);
376
+ if (node?.children) {
377
+ for (const child of node.children) {
378
+ if (typeof child === "string") {
379
+ const match = NODEREF_PATTERN.exec(child);
380
+ if (match) {
381
+ const childId = match[1];
382
+ if (dfs(childId, [...path, nodeId])) {
383
+ return true;
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ recursionStack.delete(nodeId);
390
+ return false;
391
+ }
392
+ for (const node of nodes) {
393
+ if (!visited.has(node.id)) {
394
+ dfs(node.id, []);
395
+ }
396
+ }
397
+ return errors;
398
+ }
399
+
400
+ // src/phases/03-semantic.ts
401
+ var NODEREF_PATTERN2 = /^NodeRef\(([a-z][a-z0-9_.-]*)\)$/;
402
+ function validateSemantic(spec) {
403
+ const errors = [];
404
+ const nodeIndex = /* @__PURE__ */ new Map();
405
+ const entityIds = /* @__PURE__ */ new Set();
406
+ const enumIds = /* @__PURE__ */ new Set();
407
+ const commandIds = /* @__PURE__ */ new Set();
408
+ const eventIds = /* @__PURE__ */ new Set();
409
+ const stepIds = /* @__PURE__ */ new Set();
410
+ for (const node of spec.domain.nodes) {
411
+ nodeIndex.set(node.id, node);
412
+ switch (node.kind) {
413
+ case "Entity":
414
+ entityIds.add(node.id);
415
+ break;
416
+ case "Enum":
417
+ enumIds.add(node.id);
418
+ break;
419
+ case "Command":
420
+ commandIds.add(node.id);
421
+ break;
422
+ case "Event":
423
+ eventIds.add(node.id);
424
+ break;
425
+ case "Step":
426
+ stepIds.add(node.id);
427
+ break;
428
+ }
429
+ }
430
+ spec.domain.nodes.forEach((node, index) => {
431
+ const location = `domain.nodes[${index}]`;
432
+ const nodeErrors = validateNodeSemantic(
433
+ node,
434
+ location,
435
+ { entityIds, enumIds, commandIds, eventIds, stepIds, nodeIndex }
436
+ );
437
+ errors.push(...nodeErrors);
438
+ });
439
+ return errors;
440
+ }
441
+ function validateNodeSemantic(node, location, ctx) {
442
+ const errors = [];
443
+ const spec = node.spec;
444
+ switch (node.kind) {
445
+ case "System":
446
+ if (!spec.goals || !Array.isArray(spec.goals)) {
447
+ errors.push({
448
+ code: "SEMANTIC_ERROR",
449
+ phase: 3,
450
+ level: "hard",
451
+ message: "System must have 'goals' array in spec",
452
+ location: `${location}.spec`
453
+ });
454
+ }
455
+ break;
456
+ case "Entity":
457
+ errors.push(...validateEntity(node, location, ctx));
458
+ break;
459
+ case "Enum":
460
+ if (!spec.values || !Array.isArray(spec.values)) {
461
+ errors.push({
462
+ code: "SEMANTIC_ERROR",
463
+ phase: 3,
464
+ level: "hard",
465
+ message: "Enum must have 'values' array in spec",
466
+ location: `${location}.spec`
467
+ });
468
+ }
469
+ break;
470
+ case "Command":
471
+ errors.push(...validateCommand(node, location, ctx));
472
+ break;
473
+ case "Event":
474
+ if (!spec.payload || typeof spec.payload !== "object") {
475
+ errors.push({
476
+ code: "EVENT_MISSING_PAYLOAD",
477
+ phase: 3,
478
+ level: "hard",
479
+ message: "Event must have 'payload' in spec",
480
+ location: `${location}.spec`
481
+ });
482
+ }
483
+ break;
484
+ case "Query":
485
+ if (!spec.input || typeof spec.input !== "object") {
486
+ errors.push({
487
+ code: "QUERY_MISSING_INPUT",
488
+ phase: 3,
489
+ level: "hard",
490
+ message: "Query must have 'input' in spec",
491
+ location: `${location}.spec`
492
+ });
493
+ }
494
+ if (!spec.output || typeof spec.output !== "object") {
495
+ errors.push({
496
+ code: "QUERY_MISSING_OUTPUT",
497
+ phase: 3,
498
+ level: "hard",
499
+ message: "Query must have 'output' in spec",
500
+ location: `${location}.spec`
501
+ });
502
+ }
503
+ break;
504
+ case "Process":
505
+ errors.push(...validateProcess(node, location, ctx));
506
+ break;
507
+ case "Step":
508
+ if (!spec.action || typeof spec.action !== "string") {
509
+ errors.push({
510
+ code: "SEMANTIC_ERROR",
511
+ phase: 3,
512
+ level: "hard",
513
+ message: "Step must have 'action' string in spec",
514
+ location: `${location}.spec`
515
+ });
516
+ }
517
+ break;
518
+ case "Scenario":
519
+ errors.push(...validateScenario(node, location, ctx));
520
+ break;
521
+ }
522
+ if (node.contracts) {
523
+ node.contracts.forEach((contract, cIndex) => {
524
+ if (!contract.level) {
525
+ contract.level = "hard";
526
+ }
527
+ if (!contract.type && !contract.invariant && !contract.temporal && !contract.rule) {
528
+ errors.push({
529
+ code: "INVALID_CONTRACT",
530
+ phase: 3,
531
+ level: "hard",
532
+ message: "Contract must have type, invariant, temporal, or rule",
533
+ location: `${location}.contracts[${cIndex}]`
534
+ });
535
+ }
536
+ });
537
+ }
538
+ return errors;
539
+ }
540
+ function validateEntity(node, location, ctx) {
541
+ const errors = [];
542
+ const spec = node.spec;
543
+ if (!spec.fields || typeof spec.fields !== "object") {
544
+ errors.push({
545
+ code: "ENTITY_MISSING_FIELDS",
546
+ phase: 3,
547
+ level: "hard",
548
+ message: "Entity must have 'fields' in spec",
549
+ location: `${location}.spec`
550
+ });
551
+ return errors;
552
+ }
553
+ const fields = spec.fields;
554
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
555
+ if (!fieldDef || typeof fieldDef !== "object") {
556
+ errors.push({
557
+ code: "FIELD_MISSING_TYPE",
558
+ phase: 3,
559
+ level: "hard",
560
+ message: `Field '${fieldName}' must be an object`,
561
+ location: `${location}.spec.fields.${fieldName}`
562
+ });
563
+ continue;
564
+ }
565
+ const field = fieldDef;
566
+ if (!field.type) {
567
+ errors.push({
568
+ code: "FIELD_MISSING_TYPE",
569
+ phase: 3,
570
+ level: "hard",
571
+ message: `Field '${fieldName}' missing 'type'`,
572
+ location: `${location}.spec.fields.${fieldName}`
573
+ });
574
+ continue;
575
+ }
576
+ const typeStr = field.type;
577
+ const refMatch = typeStr.match(/^ref\(([^)]+)\)$/);
578
+ const enumMatch = typeStr.match(/^enum\(([^)]+)\)$/);
579
+ if (refMatch) {
580
+ const refId = refMatch[1];
581
+ if (!ctx.entityIds.has(refId)) {
582
+ errors.push({
583
+ code: "UNRESOLVED_REF_TYPE",
584
+ phase: 3,
585
+ level: "hard",
586
+ message: `Referenced entity not found: ${refId}`,
587
+ location: `${location}.spec.fields.${fieldName}.type`
588
+ });
589
+ }
590
+ } else if (enumMatch) {
591
+ }
592
+ }
593
+ return errors;
594
+ }
595
+ function validateCommand(node, location, ctx) {
596
+ const errors = [];
597
+ const spec = node.spec;
598
+ if (!spec.input || typeof spec.input !== "object") {
599
+ errors.push({
600
+ code: "COMMAND_MISSING_INPUT",
601
+ phase: 3,
602
+ level: "hard",
603
+ message: "Command must have 'input' in spec",
604
+ location: `${location}.spec`
605
+ });
606
+ }
607
+ if (spec.effects && typeof spec.effects === "object") {
608
+ const effects = spec.effects;
609
+ if (effects.emits && Array.isArray(effects.emits)) {
610
+ effects.emits.forEach((eventId, eIndex) => {
611
+ if (!ctx.eventIds.has(eventId)) {
612
+ errors.push({
613
+ code: "UNRESOLVED_EFFECT_EVENT",
614
+ phase: 3,
615
+ level: "hard",
616
+ message: `Emitted event not found: ${eventId}`,
617
+ location: `${location}.spec.effects.emits[${eIndex}]`
618
+ });
619
+ }
620
+ });
621
+ }
622
+ if (effects.modifies && Array.isArray(effects.modifies)) {
623
+ effects.modifies.forEach((entityId, eIndex) => {
624
+ if (!ctx.entityIds.has(entityId)) {
625
+ errors.push({
626
+ code: "UNRESOLVED_EFFECT_ENTITY",
627
+ phase: 3,
628
+ level: "hard",
629
+ message: `Modified entity not found: ${entityId}`,
630
+ location: `${location}.spec.effects.modifies[${eIndex}]`
631
+ });
632
+ }
633
+ });
634
+ }
635
+ }
636
+ return errors;
637
+ }
638
+ function validateProcess(node, location, ctx) {
639
+ const errors = [];
640
+ const spec = node.spec;
641
+ if (!spec.trigger) {
642
+ errors.push({
643
+ code: "PROCESS_MISSING_TRIGGER",
644
+ phase: 3,
645
+ level: "hard",
646
+ message: "Process must have 'trigger' in spec",
647
+ location: `${location}.spec`
648
+ });
649
+ } else {
650
+ const trigger = spec.trigger;
651
+ if (!ctx.commandIds.has(trigger) && !ctx.eventIds.has(trigger)) {
652
+ errors.push({
653
+ code: "INVALID_PROCESS_TRIGGER",
654
+ phase: 3,
655
+ level: "hard",
656
+ message: `Process trigger not found: ${trigger}`,
657
+ location: `${location}.spec.trigger`
658
+ });
659
+ }
660
+ }
661
+ if (node.children) {
662
+ node.children.forEach((child, cIndex) => {
663
+ if (typeof child === "string") {
664
+ const match = NODEREF_PATTERN2.exec(child);
665
+ if (match) {
666
+ const childId = match[1];
667
+ const childNode = ctx.nodeIndex.get(childId);
668
+ if (childNode && childNode.kind !== "Step") {
669
+ errors.push({
670
+ code: "INVALID_PROCESS_CHILDREN",
671
+ phase: 3,
672
+ level: "hard",
673
+ message: `Process children must be Steps, got: ${childNode.kind}`,
674
+ location: `${location}.children[${cIndex}]`
675
+ });
676
+ }
677
+ }
678
+ }
679
+ });
680
+ }
681
+ return errors;
682
+ }
683
+ function validateScenario(node, location, ctx) {
684
+ const errors = [];
685
+ const spec = node.spec;
686
+ if (!spec.given || !Array.isArray(spec.given)) {
687
+ errors.push({
688
+ code: "SCENARIO_MISSING_GIVEN",
689
+ phase: 3,
690
+ level: "hard",
691
+ message: "Scenario must have 'given' array in spec",
692
+ location: `${location}.spec`
693
+ });
694
+ }
695
+ if (!spec.when || !Array.isArray(spec.when)) {
696
+ errors.push({
697
+ code: "SCENARIO_MISSING_WHEN",
698
+ phase: 3,
699
+ level: "hard",
700
+ message: "Scenario must have 'when' array in spec",
701
+ location: `${location}.spec`
702
+ });
703
+ }
704
+ if (!spec.then || !Array.isArray(spec.then)) {
705
+ errors.push({
706
+ code: "SCENARIO_MISSING_THEN",
707
+ phase: 3,
708
+ level: "hard",
709
+ message: "Scenario must have 'then' array in spec",
710
+ location: `${location}.spec`
711
+ });
712
+ }
713
+ return errors;
714
+ }
715
+
716
+ // src/phases/04-evolution.ts
717
+ function validateEvolution(spec) {
718
+ const errors = [];
719
+ if (!spec.history || spec.history.length === 0) {
720
+ return errors;
721
+ }
722
+ const history = spec.history;
723
+ if (history[0].basedOn !== null) {
724
+ errors.push({
725
+ code: "INVALID_HISTORY_START",
726
+ phase: 4,
727
+ level: "hard",
728
+ message: "First history entry must have basedOn: null",
729
+ location: "history[0].basedOn"
730
+ });
731
+ }
732
+ for (let i = 1; i < history.length; i++) {
733
+ const current = history[i];
734
+ const previous = history[i - 1];
735
+ if (current.basedOn !== previous.version) {
736
+ errors.push({
737
+ code: "BROKEN_HISTORY_CHAIN",
738
+ phase: 4,
739
+ level: "hard",
740
+ message: `History chain broken: ${current.version} basedOn ${current.basedOn}, expected ${previous.version}`,
741
+ location: `history[${i}].basedOn`
742
+ });
743
+ }
744
+ }
745
+ const lastVersion = history[history.length - 1].version;
746
+ if (spec.project.versioning.current !== lastVersion) {
747
+ errors.push({
748
+ code: "VERSION_MISMATCH",
749
+ phase: 4,
750
+ level: "hard",
751
+ message: `Current version ${spec.project.versioning.current} doesn't match last history version ${lastVersion}`,
752
+ location: "project.versioning.current"
753
+ });
754
+ }
755
+ history.forEach((entry, index) => {
756
+ if (entry.migrations) {
757
+ entry.migrations.forEach((migration, mIndex) => {
758
+ const mLocation = `history[${index}].migrations[${mIndex}]`;
759
+ if (!migration.id) {
760
+ errors.push({
761
+ code: "MIGRATION_MISSING_ID",
762
+ phase: 4,
763
+ level: "hard",
764
+ message: "Migration missing 'id'",
765
+ location: mLocation
766
+ });
767
+ }
768
+ if (!migration.kind) {
769
+ errors.push({
770
+ code: "MIGRATION_MISSING_KIND",
771
+ phase: 4,
772
+ level: "hard",
773
+ message: "Migration missing 'kind'",
774
+ location: mLocation
775
+ });
776
+ } else if (!["data", "schema", "process"].includes(migration.kind)) {
777
+ errors.push({
778
+ code: "INVALID_MIGRATION_KIND",
779
+ phase: 4,
780
+ level: "hard",
781
+ message: `Invalid migration kind: ${migration.kind}`,
782
+ location: `${mLocation}.kind`,
783
+ suggestion: "Valid kinds: 'data', 'schema', 'process'"
784
+ });
785
+ }
786
+ if (!migration.steps || !Array.isArray(migration.steps)) {
787
+ errors.push({
788
+ code: "MIGRATION_MISSING_STEPS",
789
+ phase: 4,
790
+ level: "hard",
791
+ message: "Migration missing 'steps'",
792
+ location: mLocation
793
+ });
794
+ }
795
+ });
796
+ }
797
+ if (entry.changes) {
798
+ const breakingOps = ["remove-field", "rename-field", "type-change", "remove-node", "rename-node"];
799
+ entry.changes.forEach((change, cIndex) => {
800
+ if (breakingOps.includes(change.op)) {
801
+ const hasMigration = entry.migrations?.some(
802
+ (m) => m.id.includes(change.target) || m.id.includes(change.field || "")
803
+ );
804
+ if (!hasMigration && (!entry.migrations || entry.migrations.length === 0)) {
805
+ errors.push({
806
+ code: "MISSING_MIGRATION",
807
+ phase: 4,
808
+ level: "hard",
809
+ message: `Breaking change '${change.op}' on '${change.target}' requires migration`,
810
+ location: `history[${index}].changes[${cIndex}]`,
811
+ suggestion: "Add a migration with steps to handle this change"
812
+ });
813
+ }
814
+ }
815
+ if (change.op === "add-field" && change.required === true) {
816
+ const hasMigration = entry.migrations?.some(
817
+ (m) => m.id.includes(change.target) || m.id.includes(change.field || "")
818
+ );
819
+ if (!hasMigration && (!entry.migrations || entry.migrations.length === 0)) {
820
+ errors.push({
821
+ code: "MISSING_MIGRATION",
822
+ phase: 4,
823
+ level: "hard",
824
+ message: `Adding required field '${change.field}' to '${change.target}' requires migration`,
825
+ location: `history[${index}].changes[${cIndex}]`,
826
+ suggestion: "Add a migration to backfill existing data"
827
+ });
828
+ }
829
+ }
830
+ });
831
+ }
832
+ });
833
+ return errors;
834
+ }
835
+
836
+ // src/phases/05-generation.ts
837
+ function validateGeneration(spec) {
838
+ const errors = [];
839
+ if (!spec.generation) {
840
+ return errors;
841
+ }
842
+ const gen = spec.generation;
843
+ if (gen.zones) {
844
+ errors.push(...validateZones(gen.zones));
845
+ }
846
+ if (gen.hooks) {
847
+ errors.push(...validateHooks(gen.hooks, gen.zones || []));
848
+ }
849
+ return errors;
850
+ }
851
+ function validateZones(zones) {
852
+ const errors = [];
853
+ const seenPaths = /* @__PURE__ */ new Set();
854
+ zones.forEach((zone, index) => {
855
+ const location = `generation.zones[${index}]`;
856
+ if (!zone.path) {
857
+ errors.push({
858
+ code: "GENERATION_ERROR",
859
+ phase: 5,
860
+ level: "hard",
861
+ message: "Zone missing 'path'",
862
+ location
863
+ });
864
+ }
865
+ if (!zone.mode) {
866
+ errors.push({
867
+ code: "GENERATION_ERROR",
868
+ phase: 5,
869
+ level: "hard",
870
+ message: "Zone missing 'mode'",
871
+ location
872
+ });
873
+ } else if (!["overwrite", "anchored", "preserve", "spec-controlled"].includes(zone.mode)) {
874
+ errors.push({
875
+ code: "GENERATION_ERROR",
876
+ phase: 5,
877
+ level: "hard",
878
+ message: `Invalid zone mode: ${zone.mode}`,
879
+ location: `${location}.mode`,
880
+ suggestion: "Valid modes: 'overwrite', 'anchored', 'preserve', 'spec-controlled'"
881
+ });
882
+ }
883
+ if (zone.path && seenPaths.has(zone.path)) {
884
+ errors.push({
885
+ code: "OVERLAPPING_ZONES",
886
+ phase: 5,
887
+ level: "hard",
888
+ message: `Duplicate zone path: ${zone.path}`,
889
+ location
890
+ });
891
+ }
892
+ if (zone.path) {
893
+ seenPaths.add(zone.path);
894
+ }
895
+ });
896
+ return errors;
897
+ }
898
+ function validateHooks(hooks, zones) {
899
+ const errors = [];
900
+ const seenIds = /* @__PURE__ */ new Set();
901
+ const overwriteZones = zones.filter((z) => z.mode === "overwrite").map((z) => z.path);
902
+ hooks.forEach((hook, index) => {
903
+ const location = `generation.hooks[${index}]`;
904
+ if (!hook.id) {
905
+ errors.push({
906
+ code: "GENERATION_ERROR",
907
+ phase: 5,
908
+ level: "hard",
909
+ message: "Hook missing 'id'",
910
+ location
911
+ });
912
+ } else {
913
+ if (seenIds.has(hook.id)) {
914
+ errors.push({
915
+ code: "DUPLICATE_HOOK_ID",
916
+ phase: 5,
917
+ level: "hard",
918
+ message: `Duplicate hook ID: ${hook.id}`,
919
+ location
920
+ });
921
+ }
922
+ seenIds.add(hook.id);
923
+ }
924
+ if (!hook.location) {
925
+ errors.push({
926
+ code: "GENERATION_ERROR",
927
+ phase: 5,
928
+ level: "hard",
929
+ message: "Hook missing 'location'",
930
+ location
931
+ });
932
+ } else {
933
+ if (!hook.location.file) {
934
+ errors.push({
935
+ code: "GENERATION_ERROR",
936
+ phase: 5,
937
+ level: "hard",
938
+ message: "Hook location missing 'file'",
939
+ location: `${location}.location`
940
+ });
941
+ }
942
+ if (!hook.location.anchorStart || !hook.location.anchorEnd) {
943
+ errors.push({
944
+ code: "INVALID_HOOK_ANCHORS",
945
+ phase: 5,
946
+ level: "hard",
947
+ message: "Hook location missing 'anchorStart' or 'anchorEnd'",
948
+ location: `${location}.location`
949
+ });
950
+ } else if (hook.location.anchorStart === hook.location.anchorEnd) {
951
+ errors.push({
952
+ code: "INVALID_HOOK_ANCHORS",
953
+ phase: 5,
954
+ level: "hard",
955
+ message: "Hook anchorStart and anchorEnd must be different",
956
+ location: `${location}.location`
957
+ });
958
+ }
959
+ if (hook.location.file) {
960
+ for (const zonePath of overwriteZones) {
961
+ if (matchesGlob(hook.location.file, zonePath)) {
962
+ errors.push({
963
+ code: "HOOK_IN_OVERWRITE",
964
+ phase: 5,
965
+ level: "hard",
966
+ message: `Hook '${hook.id}' is in overwrite zone: ${zonePath}`,
967
+ location: `${location}.location.file`,
968
+ suggestion: "Move hook to an anchored zone"
969
+ });
970
+ break;
971
+ }
972
+ }
973
+ }
974
+ }
975
+ });
976
+ return errors;
977
+ }
978
+ function matchesGlob(path, pattern) {
979
+ const normalizedPath = path.replace(/\\/g, "/");
980
+ const normalizedPattern = pattern.replace(/\\/g, "/");
981
+ const regexPattern = normalizedPattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*").replace(/\//g, "\\/");
982
+ const regex = new RegExp(`^${regexPattern}$`);
983
+ return regex.test(normalizedPath);
984
+ }
985
+
986
+ // src/phases/06-verifiability.ts
987
+ function validateVerifiability(spec) {
988
+ const errors = [];
989
+ if (spec.generation?.pipelines) {
990
+ const pipelines = spec.generation.pipelines;
991
+ if (!pipelines.build) {
992
+ errors.push({
993
+ code: "MISSING_BUILD_PIPELINE",
994
+ phase: 6,
995
+ level: "hard",
996
+ message: "Missing required 'build' pipeline",
997
+ location: "generation.pipelines"
998
+ });
999
+ } else if (!pipelines.build.cmd || pipelines.build.cmd.trim() === "") {
1000
+ errors.push({
1001
+ code: "EMPTY_PIPELINE_CMD",
1002
+ phase: 6,
1003
+ level: "hard",
1004
+ message: "'build' pipeline has empty command",
1005
+ location: "generation.pipelines.build.cmd"
1006
+ });
1007
+ }
1008
+ if (!pipelines.test) {
1009
+ errors.push({
1010
+ code: "MISSING_TEST_PIPELINE",
1011
+ phase: 6,
1012
+ level: "hard",
1013
+ message: "Missing required 'test' pipeline",
1014
+ location: "generation.pipelines"
1015
+ });
1016
+ } else if (!pipelines.test.cmd || pipelines.test.cmd.trim() === "") {
1017
+ errors.push({
1018
+ code: "EMPTY_PIPELINE_CMD",
1019
+ phase: 6,
1020
+ level: "hard",
1021
+ message: "'test' pipeline has empty command",
1022
+ location: "generation.pipelines.test.cmd"
1023
+ });
1024
+ }
1025
+ if (!pipelines.migrate) {
1026
+ errors.push({
1027
+ code: "MISSING_MIGRATE_PIPELINE",
1028
+ phase: 6,
1029
+ level: "hard",
1030
+ message: "Missing required 'migrate' pipeline",
1031
+ location: "generation.pipelines"
1032
+ });
1033
+ } else if (!pipelines.migrate.cmd || pipelines.migrate.cmd.trim() === "") {
1034
+ errors.push({
1035
+ code: "EMPTY_PIPELINE_CMD",
1036
+ phase: 6,
1037
+ level: "hard",
1038
+ message: "'migrate' pipeline has empty command",
1039
+ location: "generation.pipelines.migrate.cmd"
1040
+ });
1041
+ }
1042
+ }
1043
+ const commands = spec.domain.nodes.filter((n) => n.kind === "Command");
1044
+ const scenarios = spec.domain.nodes.filter((n) => n.kind === "Scenario");
1045
+ const coveredCommands = /* @__PURE__ */ new Set();
1046
+ for (const scenario of scenarios) {
1047
+ const scenarioSpec = scenario.spec;
1048
+ if (scenarioSpec.when && Array.isArray(scenarioSpec.when)) {
1049
+ for (const action of scenarioSpec.when) {
1050
+ const actionObj = action;
1051
+ if (actionObj.command && typeof actionObj.command === "object") {
1052
+ const cmd = actionObj.command;
1053
+ if (cmd.id) {
1054
+ coveredCommands.add(cmd.id);
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+ for (const command of commands) {
1061
+ if (!coveredCommands.has(command.id)) {
1062
+ errors.push({
1063
+ code: "LOW_SCENARIO_COVERAGE",
1064
+ phase: 6,
1065
+ level: "soft",
1066
+ message: `Command '${command.id}' has no test scenarios`,
1067
+ location: `domain.nodes`,
1068
+ suggestion: `Add a Scenario that tests ${command.id}`
1069
+ });
1070
+ }
1071
+ }
1072
+ return errors;
1073
+ }
1074
+
1075
+ // src/index.ts
1076
+ function validate(spec, options = {}) {
1077
+ const { phases = [1, 2, 3, 4, 5, 6], strict = false } = options;
1078
+ const allErrors = [];
1079
+ let currentPhase = 1;
1080
+ if (phases.includes(1)) {
1081
+ currentPhase = 1;
1082
+ const errors = validateStructural(spec);
1083
+ allErrors.push(...errors);
1084
+ if (hasHardErrors(errors)) {
1085
+ return buildResult(allErrors, currentPhase, strict);
1086
+ }
1087
+ }
1088
+ const evoSpec = spec;
1089
+ if (phases.includes(2)) {
1090
+ currentPhase = 2;
1091
+ const errors = validateReferential(evoSpec);
1092
+ allErrors.push(...errors);
1093
+ if (hasHardErrors(errors)) {
1094
+ return buildResult(allErrors, currentPhase, strict);
1095
+ }
1096
+ }
1097
+ if (phases.includes(3)) {
1098
+ currentPhase = 3;
1099
+ const errors = validateSemantic(evoSpec);
1100
+ allErrors.push(...errors);
1101
+ if (hasHardErrors(errors)) {
1102
+ return buildResult(allErrors, currentPhase, strict);
1103
+ }
1104
+ }
1105
+ if (phases.includes(4)) {
1106
+ currentPhase = 4;
1107
+ const errors = validateEvolution(evoSpec);
1108
+ allErrors.push(...errors);
1109
+ if (hasHardErrors(errors)) {
1110
+ return buildResult(allErrors, currentPhase, strict);
1111
+ }
1112
+ }
1113
+ if (phases.includes(5)) {
1114
+ currentPhase = 5;
1115
+ const errors = validateGeneration(evoSpec);
1116
+ allErrors.push(...errors);
1117
+ if (hasHardErrors(errors)) {
1118
+ return buildResult(allErrors, currentPhase, strict);
1119
+ }
1120
+ }
1121
+ if (phases.includes(6)) {
1122
+ currentPhase = 6;
1123
+ const errors = validateVerifiability(evoSpec);
1124
+ allErrors.push(...errors);
1125
+ }
1126
+ return buildResult(allErrors, currentPhase, strict);
1127
+ }
1128
+ function validatePhase(spec, phase) {
1129
+ switch (phase) {
1130
+ case 1:
1131
+ return validateStructural(spec);
1132
+ case 2:
1133
+ return validateReferential(spec);
1134
+ case 3:
1135
+ return validateSemantic(spec);
1136
+ case 4:
1137
+ return validateEvolution(spec);
1138
+ case 5:
1139
+ return validateGeneration(spec);
1140
+ case 6:
1141
+ return validateVerifiability(spec);
1142
+ default:
1143
+ throw new Error(`Invalid phase: ${phase}`);
1144
+ }
1145
+ }
1146
+ function parseSpec(yaml) {
1147
+ return (0, import_yaml.parse)(yaml);
1148
+ }
1149
+ function validateYaml(yaml, options = {}) {
1150
+ try {
1151
+ const spec = parseSpec(yaml);
1152
+ return validate(spec, options);
1153
+ } catch (error) {
1154
+ return {
1155
+ ok: false,
1156
+ errors: [{
1157
+ code: "STRUCTURAL_ERROR",
1158
+ phase: 1,
1159
+ level: "hard",
1160
+ message: `Failed to parse YAML: ${error.message}`,
1161
+ location: ""
1162
+ }],
1163
+ warnings: [],
1164
+ phase: 1
1165
+ };
1166
+ }
1167
+ }
1168
+ function hasHardErrors(errors) {
1169
+ return errors.some((e) => e.level === "hard");
1170
+ }
1171
+ function buildResult(errors, phase, strict) {
1172
+ const hardErrors = errors.filter((e) => e.level === "hard");
1173
+ const softErrors = errors.filter((e) => e.level === "soft");
1174
+ if (strict) {
1175
+ return {
1176
+ ok: errors.length === 0,
1177
+ errors,
1178
+ warnings: [],
1179
+ phase
1180
+ };
1181
+ }
1182
+ return {
1183
+ ok: hardErrors.length === 0,
1184
+ errors: hardErrors,
1185
+ warnings: softErrors,
1186
+ phase
1187
+ };
1188
+ }
1189
+ // Annotate the CommonJS export names for ESM import in node:
1190
+ 0 && (module.exports = {
1191
+ parseSpec,
1192
+ validate,
1193
+ validatePhase,
1194
+ validateYaml
1195
+ });