@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.d.mts +204 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +1195 -0
- package/dist/index.mjs +1167 -0
- package/package.json +57 -0
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
|
+
});
|