ai-spec-dev 0.1.0 → 0.17.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/.claude/settings.local.json +18 -0
- package/README.md +1215 -146
- package/RELEASE_LOG.md +1489 -0
- package/cli/index.ts +1981 -0
- package/cli/welcome.ts +151 -0
- package/core/code-generator.ts +757 -0
- package/core/combined-generator.ts +63 -0
- package/core/constitution-consolidator.ts +141 -0
- package/core/constitution-generator.ts +89 -0
- package/core/context-loader.ts +453 -0
- package/core/contract-bridge.ts +217 -0
- package/core/dsl-extractor.ts +337 -0
- package/core/dsl-types.ts +166 -0
- package/core/dsl-validator.ts +450 -0
- package/core/error-feedback.ts +354 -0
- package/core/frontend-context-loader.ts +602 -0
- package/core/global-constitution.ts +88 -0
- package/core/key-store.ts +49 -0
- package/core/knowledge-memory.ts +171 -0
- package/core/mock-server-generator.ts +571 -0
- package/core/openapi-exporter.ts +361 -0
- package/core/requirement-decomposer.ts +198 -0
- package/core/reviewer.ts +259 -0
- package/core/spec-assessor.ts +99 -0
- package/core/spec-generator.ts +428 -0
- package/core/spec-refiner.ts +89 -0
- package/core/spec-updater.ts +227 -0
- package/core/spec-versioning.ts +213 -0
- package/core/task-generator.ts +174 -0
- package/core/test-generator.ts +273 -0
- package/core/workspace-loader.ts +256 -0
- package/dist/cli/index.js +6717 -672
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6717 -670
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +147 -27
- package/dist/index.d.ts +147 -27
- package/dist/index.js +2337 -286
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2329 -285
- package/dist/index.mjs.map +1 -1
- package/git/worktree.ts +109 -0
- package/index.ts +9 -0
- package/package.json +4 -28
- package/prompts/codegen.prompt.ts +259 -0
- package/prompts/consolidate.prompt.ts +73 -0
- package/prompts/constitution.prompt.ts +63 -0
- package/prompts/decompose.prompt.ts +168 -0
- package/prompts/dsl.prompt.ts +203 -0
- package/prompts/frontend-spec.prompt.ts +191 -0
- package/prompts/global-constitution.prompt.ts +61 -0
- package/prompts/spec-assess.prompt.ts +53 -0
- package/prompts/spec.prompt.ts +102 -0
- package/prompts/tasks.prompt.ts +35 -0
- package/prompts/testgen.prompt.ts +84 -0
- package/prompts/update.prompt.ts +131 -0
- package/purpose.docx +0 -0
- package/purpose.md +444 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL Schema Validator — no external dependencies, no recursion.
|
|
3
|
+
*
|
|
4
|
+
* Safety design:
|
|
5
|
+
* - All loops are bounded by finite array lengths.
|
|
6
|
+
* - No recursive function calls.
|
|
7
|
+
* - Collects ALL errors in one pass instead of throwing on first failure.
|
|
8
|
+
* - Never mutates the input.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import {
|
|
13
|
+
SpecDSL,
|
|
14
|
+
DslValidationError,
|
|
15
|
+
DslValidationResult,
|
|
16
|
+
HttpMethod,
|
|
17
|
+
} from "./dsl-types";
|
|
18
|
+
|
|
19
|
+
const VALID_METHODS: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
20
|
+
const MAX_MODELS = 50;
|
|
21
|
+
const MAX_FIELDS_PER_MODEL = 100;
|
|
22
|
+
const MAX_ENDPOINTS = 100;
|
|
23
|
+
const MAX_BEHAVIORS = 50;
|
|
24
|
+
const MAX_ERRORS_PER_ENDPOINT = 20;
|
|
25
|
+
|
|
26
|
+
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export function validateDsl(raw: unknown): DslValidationResult {
|
|
29
|
+
const errors: DslValidationError[] = [];
|
|
30
|
+
|
|
31
|
+
// Guard: must be a plain object
|
|
32
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
errors: [{ path: "root", message: "DSL must be a JSON object, got: " + typeLabel(raw) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const obj = raw as Record<string, unknown>;
|
|
40
|
+
|
|
41
|
+
// ── version ────────────────────────────────────────────────────────────────
|
|
42
|
+
if (obj["version"] !== "1.0") {
|
|
43
|
+
errors.push({
|
|
44
|
+
path: "version",
|
|
45
|
+
message: `Must be "1.0", got: ${JSON.stringify(obj["version"])}`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── feature ────────────────────────────────────────────────────────────────
|
|
50
|
+
validateFeature(obj["feature"], "feature", errors);
|
|
51
|
+
|
|
52
|
+
// ── models ─────────────────────────────────────────────────────────────────
|
|
53
|
+
if (!Array.isArray(obj["models"])) {
|
|
54
|
+
errors.push({ path: "models", message: `Must be an array, got: ${typeLabel(obj["models"])}` });
|
|
55
|
+
} else {
|
|
56
|
+
const models = obj["models"] as unknown[];
|
|
57
|
+
if (models.length > MAX_MODELS) {
|
|
58
|
+
errors.push({ path: "models", message: `Too many models (${models.length} > ${MAX_MODELS})` });
|
|
59
|
+
}
|
|
60
|
+
// Bounded loop — no recursion
|
|
61
|
+
for (let i = 0; i < Math.min(models.length, MAX_MODELS); i++) {
|
|
62
|
+
validateModel(models[i], `models[${i}]`, errors);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── endpoints ──────────────────────────────────────────────────────────────
|
|
67
|
+
if (!Array.isArray(obj["endpoints"])) {
|
|
68
|
+
errors.push({ path: "endpoints", message: `Must be an array, got: ${typeLabel(obj["endpoints"])}` });
|
|
69
|
+
} else {
|
|
70
|
+
const eps = obj["endpoints"] as unknown[];
|
|
71
|
+
if (eps.length > MAX_ENDPOINTS) {
|
|
72
|
+
errors.push({ path: "endpoints", message: `Too many endpoints (${eps.length} > ${MAX_ENDPOINTS})` });
|
|
73
|
+
}
|
|
74
|
+
for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
|
|
75
|
+
validateEndpoint(eps[i], `endpoints[${i}]`, errors);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── behaviors (optional, but must be array if present) ────────────────────
|
|
80
|
+
if (obj["behaviors"] !== undefined) {
|
|
81
|
+
if (!Array.isArray(obj["behaviors"])) {
|
|
82
|
+
errors.push({ path: "behaviors", message: `Must be an array if present, got: ${typeLabel(obj["behaviors"])}` });
|
|
83
|
+
} else {
|
|
84
|
+
const behaviors = obj["behaviors"] as unknown[];
|
|
85
|
+
if (behaviors.length > MAX_BEHAVIORS) {
|
|
86
|
+
errors.push({ path: "behaviors", message: `Too many behaviors (${behaviors.length} > ${MAX_BEHAVIORS})` });
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < Math.min(behaviors.length, MAX_BEHAVIORS); i++) {
|
|
89
|
+
validateBehavior(behaviors[i], `behaviors[${i}]`, errors);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── components (optional, frontend only) ──────────────────────────────────
|
|
95
|
+
if (obj["components"] !== undefined) {
|
|
96
|
+
if (!Array.isArray(obj["components"])) {
|
|
97
|
+
errors.push({ path: "components", message: `Must be an array if present, got: ${typeLabel(obj["components"])}` });
|
|
98
|
+
} else {
|
|
99
|
+
const components = obj["components"] as unknown[];
|
|
100
|
+
for (let i = 0; i < Math.min(components.length, 50); i++) {
|
|
101
|
+
validateComponent(components[i], `components[${i}]`, errors);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (errors.length > 0) {
|
|
107
|
+
return { valid: false, errors };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { valid: true, dsl: raw as SpecDSL };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Section validators (all iterative, no recursion) ────────────────────────
|
|
114
|
+
|
|
115
|
+
function validateFeature(
|
|
116
|
+
raw: unknown,
|
|
117
|
+
path: string,
|
|
118
|
+
errors: DslValidationError[]
|
|
119
|
+
): void {
|
|
120
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
121
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const f = raw as Record<string, unknown>;
|
|
125
|
+
requireNonEmptyString(f["id"], `${path}.id`, errors);
|
|
126
|
+
requireNonEmptyString(f["title"], `${path}.title`, errors);
|
|
127
|
+
requireNonEmptyString(f["description"], `${path}.description`, errors);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateModel(
|
|
131
|
+
raw: unknown,
|
|
132
|
+
path: string,
|
|
133
|
+
errors: DslValidationError[]
|
|
134
|
+
): void {
|
|
135
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
136
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const m = raw as Record<string, unknown>;
|
|
140
|
+
requireNonEmptyString(m["name"], `${path}.name`, errors);
|
|
141
|
+
|
|
142
|
+
if (!Array.isArray(m["fields"])) {
|
|
143
|
+
errors.push({ path: `${path}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
144
|
+
} else {
|
|
145
|
+
const fields = m["fields"] as unknown[];
|
|
146
|
+
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
147
|
+
errors.push({ path: `${path}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
148
|
+
}
|
|
149
|
+
for (let j = 0; j < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j++) {
|
|
150
|
+
validateModelField(fields[j], `${path}.fields[${j}]`, errors);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// relations: optional array of strings
|
|
155
|
+
if (m["relations"] !== undefined) {
|
|
156
|
+
if (!Array.isArray(m["relations"])) {
|
|
157
|
+
errors.push({ path: `${path}.relations`, message: "Must be an array of strings if present" });
|
|
158
|
+
} else {
|
|
159
|
+
const rels = m["relations"] as unknown[];
|
|
160
|
+
for (let j = 0; j < rels.length; j++) {
|
|
161
|
+
if (typeof rels[j] !== "string") {
|
|
162
|
+
errors.push({ path: `${path}.relations[${j}]`, message: "Must be a string" });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateModelField(
|
|
170
|
+
raw: unknown,
|
|
171
|
+
path: string,
|
|
172
|
+
errors: DslValidationError[]
|
|
173
|
+
): void {
|
|
174
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
175
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const f = raw as Record<string, unknown>;
|
|
179
|
+
requireNonEmptyString(f["name"], `${path}.name`, errors);
|
|
180
|
+
requireNonEmptyString(f["type"], `${path}.type`, errors);
|
|
181
|
+
if (typeof f["required"] !== "boolean") {
|
|
182
|
+
errors.push({ path: `${path}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateEndpoint(
|
|
187
|
+
raw: unknown,
|
|
188
|
+
path: string,
|
|
189
|
+
errors: DslValidationError[]
|
|
190
|
+
): void {
|
|
191
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
192
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const e = raw as Record<string, unknown>;
|
|
196
|
+
|
|
197
|
+
requireNonEmptyString(e["id"], `${path}.id`, errors);
|
|
198
|
+
requireNonEmptyString(e["description"], `${path}.description`, errors);
|
|
199
|
+
|
|
200
|
+
// method
|
|
201
|
+
if (!VALID_METHODS.includes(e["method"] as HttpMethod)) {
|
|
202
|
+
errors.push({
|
|
203
|
+
path: `${path}.method`,
|
|
204
|
+
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// path must be a string starting with "/"
|
|
209
|
+
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
210
|
+
errors.push({
|
|
211
|
+
path: `${path}.path`,
|
|
212
|
+
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// auth
|
|
217
|
+
if (typeof e["auth"] !== "boolean") {
|
|
218
|
+
errors.push({ path: `${path}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// successStatus
|
|
222
|
+
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
223
|
+
errors.push({
|
|
224
|
+
path: `${path}.successStatus`,
|
|
225
|
+
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
requireNonEmptyString(e["successDescription"], `${path}.successDescription`, errors);
|
|
230
|
+
|
|
231
|
+
// request: optional
|
|
232
|
+
if (e["request"] !== undefined) {
|
|
233
|
+
validateRequestSchema(e["request"], `${path}.request`, errors);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// errors: optional array
|
|
237
|
+
if (e["errors"] !== undefined) {
|
|
238
|
+
if (!Array.isArray(e["errors"])) {
|
|
239
|
+
errors.push({ path: `${path}.errors`, message: "Must be an array if present" });
|
|
240
|
+
} else {
|
|
241
|
+
const errs = e["errors"] as unknown[];
|
|
242
|
+
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
243
|
+
errors.push({ path: `${path}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
244
|
+
}
|
|
245
|
+
for (let j = 0; j < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j++) {
|
|
246
|
+
validateResponseError(errs[j], `${path}.errors[${j}]`, errors);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function validateRequestSchema(
|
|
253
|
+
raw: unknown,
|
|
254
|
+
path: string,
|
|
255
|
+
errors: DslValidationError[]
|
|
256
|
+
): void {
|
|
257
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
258
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const r = raw as Record<string, unknown>;
|
|
262
|
+
// Each of body/query/params must be a flat Record<string,string> if present
|
|
263
|
+
for (const key of ["body", "query", "params"] as const) {
|
|
264
|
+
if (r[key] !== undefined) {
|
|
265
|
+
validateFieldMap(r[key], `${path}.${key}`, errors);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function validateFieldMap(
|
|
271
|
+
raw: unknown,
|
|
272
|
+
path: string,
|
|
273
|
+
errors: DslValidationError[]
|
|
274
|
+
): void {
|
|
275
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
276
|
+
errors.push({ path, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const map = raw as Record<string, unknown>;
|
|
280
|
+
// All values must be strings
|
|
281
|
+
for (const [k, v] of Object.entries(map)) {
|
|
282
|
+
if (typeof v !== "string") {
|
|
283
|
+
errors.push({ path: `${path}.${k}`, message: `Value must be a type-description string, got: ${typeLabel(v)}` });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function validateResponseError(
|
|
289
|
+
raw: unknown,
|
|
290
|
+
path: string,
|
|
291
|
+
errors: DslValidationError[]
|
|
292
|
+
): void {
|
|
293
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
294
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const e = raw as Record<string, unknown>;
|
|
298
|
+
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
299
|
+
errors.push({ path: `${path}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
300
|
+
}
|
|
301
|
+
requireNonEmptyString(e["code"], `${path}.code`, errors);
|
|
302
|
+
requireNonEmptyString(e["description"], `${path}.description`, errors);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function validateBehavior(
|
|
306
|
+
raw: unknown,
|
|
307
|
+
path: string,
|
|
308
|
+
errors: DslValidationError[]
|
|
309
|
+
): void {
|
|
310
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
311
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const b = raw as Record<string, unknown>;
|
|
315
|
+
requireNonEmptyString(b["id"], `${path}.id`, errors);
|
|
316
|
+
requireNonEmptyString(b["description"], `${path}.description`, errors);
|
|
317
|
+
// constraints: optional array of strings
|
|
318
|
+
if (b["constraints"] !== undefined) {
|
|
319
|
+
if (!Array.isArray(b["constraints"])) {
|
|
320
|
+
errors.push({ path: `${path}.constraints`, message: "Must be an array of strings if present" });
|
|
321
|
+
} else {
|
|
322
|
+
const cs = b["constraints"] as unknown[];
|
|
323
|
+
for (let j = 0; j < cs.length; j++) {
|
|
324
|
+
if (typeof cs[j] !== "string") {
|
|
325
|
+
errors.push({ path: `${path}.constraints[${j}]`, message: "Must be a string" });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validateComponent(
|
|
333
|
+
raw: unknown,
|
|
334
|
+
path: string,
|
|
335
|
+
errors: DslValidationError[]
|
|
336
|
+
): void {
|
|
337
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
338
|
+
errors.push({ path, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const c = raw as Record<string, unknown>;
|
|
342
|
+
requireNonEmptyString(c["id"], `${path}.id`, errors);
|
|
343
|
+
requireNonEmptyString(c["name"], `${path}.name`, errors);
|
|
344
|
+
requireNonEmptyString(c["description"], `${path}.description`, errors);
|
|
345
|
+
|
|
346
|
+
// props: array of {name, type, required}
|
|
347
|
+
if (c["props"] !== undefined) {
|
|
348
|
+
if (!Array.isArray(c["props"])) {
|
|
349
|
+
errors.push({ path: `${path}.props`, message: "Must be an array if present" });
|
|
350
|
+
} else {
|
|
351
|
+
const props = c["props"] as unknown[];
|
|
352
|
+
for (let j = 0; j < props.length; j++) {
|
|
353
|
+
const p = props[j] as Record<string, unknown>;
|
|
354
|
+
if (typeof p !== "object" || p === null) {
|
|
355
|
+
errors.push({ path: `${path}.props[${j}]`, message: "Must be an object" });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
requireNonEmptyString(p["name"], `${path}.props[${j}].name`, errors);
|
|
359
|
+
requireNonEmptyString(p["type"], `${path}.props[${j}].type`, errors);
|
|
360
|
+
if (typeof p["required"] !== "boolean") {
|
|
361
|
+
errors.push({ path: `${path}.props[${j}].required`, message: "Must be boolean" });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// events: array of {name, payload?}
|
|
368
|
+
if (c["events"] !== undefined) {
|
|
369
|
+
if (!Array.isArray(c["events"])) {
|
|
370
|
+
errors.push({ path: `${path}.events`, message: "Must be an array if present" });
|
|
371
|
+
} else {
|
|
372
|
+
const events = c["events"] as unknown[];
|
|
373
|
+
for (let j = 0; j < events.length; j++) {
|
|
374
|
+
const e = events[j] as Record<string, unknown>;
|
|
375
|
+
if (typeof e !== "object" || e === null) {
|
|
376
|
+
errors.push({ path: `${path}.events[${j}]`, message: "Must be an object" });
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
requireNonEmptyString(e["name"], `${path}.events[${j}].name`, errors);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// state: Record<string, string>
|
|
385
|
+
if (c["state"] !== undefined) {
|
|
386
|
+
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
387
|
+
errors.push({ path: `${path}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// apiCalls: string[]
|
|
392
|
+
if (c["apiCalls"] !== undefined) {
|
|
393
|
+
if (!Array.isArray(c["apiCalls"])) {
|
|
394
|
+
errors.push({ path: `${path}.apiCalls`, message: "Must be an array of strings if present" });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
function requireNonEmptyString(
|
|
402
|
+
v: unknown,
|
|
403
|
+
path: string,
|
|
404
|
+
errors: DslValidationError[]
|
|
405
|
+
): void {
|
|
406
|
+
if (typeof v !== "string" || v.trim().length === 0) {
|
|
407
|
+
errors.push({
|
|
408
|
+
path,
|
|
409
|
+
message: `Must be a non-empty string, got: ${typeLabel(v)}`,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function typeLabel(v: unknown): string {
|
|
415
|
+
if (v === null) return "null";
|
|
416
|
+
if (Array.isArray(v)) return "array";
|
|
417
|
+
return typeof v;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── Pretty printer ───────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
export function printValidationErrors(errors: DslValidationError[]): void {
|
|
423
|
+
console.log(chalk.red(`\n DSL Validation failed — ${errors.length} error(s):\n`));
|
|
424
|
+
for (const err of errors) {
|
|
425
|
+
console.log(chalk.red(` ✘ ${chalk.bold(err.path)}: ${err.message}`));
|
|
426
|
+
}
|
|
427
|
+
console.log();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function printDslSummary(dsl: SpecDSL): void {
|
|
431
|
+
console.log(chalk.green(" ✔ DSL valid"));
|
|
432
|
+
console.log(chalk.gray(` Models : ${dsl.models.length}`));
|
|
433
|
+
console.log(chalk.gray(` Endpoints : ${dsl.endpoints.length}`));
|
|
434
|
+
console.log(chalk.gray(` Behaviors : ${dsl.behaviors.length}`));
|
|
435
|
+
if (dsl.components && dsl.components.length > 0) {
|
|
436
|
+
console.log(chalk.gray(` Components: ${dsl.components.length}`));
|
|
437
|
+
for (const cmp of dsl.components) {
|
|
438
|
+
console.log(chalk.gray(` ${cmp.id} ${cmp.name} — props:${cmp.props.length} events:${cmp.events.length}`));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (dsl.endpoints.length > 0) {
|
|
442
|
+
for (const ep of dsl.endpoints) {
|
|
443
|
+
const auth = ep.auth ? chalk.yellow(" [auth]") : "";
|
|
444
|
+
console.log(chalk.gray(` ${ep.method.padEnd(6)} ${ep.path}${auth} — ${ep.description}`));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Re-export for convenience
|
|
450
|
+
export type { SpecDSL, DslValidationResult, DslValidationError } from "./dsl-types";
|