ai-spec-dev 0.1.0 → 0.14.1
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 +1211 -146
- package/RELEASE_LOG.md +1444 -0
- package/cli/index.ts +1961 -0
- package/cli/welcome.ts +151 -0
- package/core/code-generator.ts +740 -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,337 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as fs from "fs-extra";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import { AIProvider } from "./spec-generator";
|
|
6
|
+
import { SpecDSL } from "./dsl-types";
|
|
7
|
+
import { validateDsl, printValidationErrors, printDslSummary } from "./dsl-validator";
|
|
8
|
+
import {
|
|
9
|
+
dslSystemPrompt,
|
|
10
|
+
dslFrontendSystemPrompt,
|
|
11
|
+
buildDslExtractionPrompt,
|
|
12
|
+
buildDslRetryPrompt,
|
|
13
|
+
} from "../prompts/dsl.prompt";
|
|
14
|
+
|
|
15
|
+
// ─── DSL Sanitizer ───────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Strips obviously-invalid entries from AI-generated DSL before validation,
|
|
19
|
+
* preventing phantom errors from causing unnecessary retries.
|
|
20
|
+
* - Removes endpoint.errors entries where code or description is empty/missing.
|
|
21
|
+
*/
|
|
22
|
+
function sanitizeDsl(raw: unknown): unknown {
|
|
23
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
24
|
+
const dsl = raw as Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(dsl["endpoints"])) {
|
|
27
|
+
dsl["endpoints"] = (dsl["endpoints"] as unknown[]).map((ep) => {
|
|
28
|
+
if (ep === null || typeof ep !== "object" || Array.isArray(ep)) return ep;
|
|
29
|
+
const endpoint = ep as Record<string, unknown>;
|
|
30
|
+
if (Array.isArray(endpoint["errors"])) {
|
|
31
|
+
endpoint["errors"] = (endpoint["errors"] as unknown[]).filter((err) => {
|
|
32
|
+
if (err === null || typeof err !== "object" || Array.isArray(err)) return false;
|
|
33
|
+
const e = err as Record<string, unknown>;
|
|
34
|
+
return typeof e["code"] === "string" && e["code"].trim().length > 0 &&
|
|
35
|
+
typeof e["description"] === "string" && e["description"].trim().length > 0;
|
|
36
|
+
});
|
|
37
|
+
if ((endpoint["errors"] as unknown[]).length === 0) {
|
|
38
|
+
delete endpoint["errors"];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return endpoint;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return dsl;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** Maximum AI attempts (1 initial + up to this many retries). */
|
|
51
|
+
const MAX_RETRIES = 2;
|
|
52
|
+
|
|
53
|
+
/** Maximum spec length passed to AI to avoid token/context blow-up. */
|
|
54
|
+
const MAX_SPEC_CHARS = 12_000;
|
|
55
|
+
|
|
56
|
+
// ─── DSL file naming ──────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export function dslFilePath(specFilePath: string): string {
|
|
59
|
+
const dir = path.dirname(specFilePath);
|
|
60
|
+
const base = path.basename(specFilePath, ".md");
|
|
61
|
+
return path.join(dir, `${base}.dsl.json`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse JSON from raw AI output.
|
|
68
|
+
* Handles two cases:
|
|
69
|
+
* 1. Bare JSON object starting with `{`
|
|
70
|
+
* 2. JSON wrapped in a ```json ... ``` fence (model sometimes ignores instructions)
|
|
71
|
+
*
|
|
72
|
+
* Does NOT use eval or Function() — only JSON.parse().
|
|
73
|
+
*/
|
|
74
|
+
function parseJsonFromOutput(raw: string): unknown {
|
|
75
|
+
const trimmed = raw.trim();
|
|
76
|
+
|
|
77
|
+
// Case 1: bare JSON
|
|
78
|
+
if (trimmed.startsWith("{")) {
|
|
79
|
+
return JSON.parse(trimmed);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Case 2: fenced JSON — extract content between first ``` and last ```
|
|
83
|
+
const fenceStart = trimmed.indexOf("```");
|
|
84
|
+
if (fenceStart !== -1) {
|
|
85
|
+
const afterFence = trimmed.slice(fenceStart + 3);
|
|
86
|
+
// Skip optional language tag (e.g. "json\n")
|
|
87
|
+
const newlinePos = afterFence.indexOf("\n");
|
|
88
|
+
const jsonStart = newlinePos !== -1 ? newlinePos + 1 : 0;
|
|
89
|
+
const fenceEnd = afterFence.lastIndexOf("```");
|
|
90
|
+
if (fenceEnd > jsonStart) {
|
|
91
|
+
const jsonStr = afterFence.slice(jsonStart, fenceEnd).trim();
|
|
92
|
+
return JSON.parse(jsonStr);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Case 3: try to find the first `{` and last `}` pair
|
|
97
|
+
const objStart = trimmed.indexOf("{");
|
|
98
|
+
const objEnd = trimmed.lastIndexOf("}");
|
|
99
|
+
if (objStart !== -1 && objEnd > objStart) {
|
|
100
|
+
return JSON.parse(trimmed.slice(objStart, objEnd + 1));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new SyntaxError("No JSON object found in AI output");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── DslExtractor ────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export class DslExtractor {
|
|
109
|
+
constructor(private provider: AIProvider) {}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract and validate a SpecDSL from the given spec content.
|
|
113
|
+
*
|
|
114
|
+
* Flow:
|
|
115
|
+
* attempt 1 → validate → if fail, show errors
|
|
116
|
+
* attempt 2 (retry with errors) → validate → if fail, show errors
|
|
117
|
+
* after MAX_RETRIES failures → prompt user: skip / abort
|
|
118
|
+
*
|
|
119
|
+
* Returns:
|
|
120
|
+
* - SpecDSL if extraction succeeded
|
|
121
|
+
* - null if user chose to skip (continue without DSL)
|
|
122
|
+
* - throws if user chose to abort
|
|
123
|
+
*/
|
|
124
|
+
async extract(
|
|
125
|
+
specContent: string,
|
|
126
|
+
opts: { auto?: boolean; isFrontend?: boolean } = {}
|
|
127
|
+
): Promise<SpecDSL | null> {
|
|
128
|
+
// Truncate very long specs to avoid token issues
|
|
129
|
+
const specForAI =
|
|
130
|
+
specContent.length > MAX_SPEC_CHARS
|
|
131
|
+
? specContent.slice(0, MAX_SPEC_CHARS) + "\n... (truncated for DSL extraction)"
|
|
132
|
+
: specContent;
|
|
133
|
+
|
|
134
|
+
let lastRawOutput = "";
|
|
135
|
+
let lastErrors: Array<{ path: string; message: string }> = [];
|
|
136
|
+
|
|
137
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
138
|
+
const isRetry = attempt > 1;
|
|
139
|
+
|
|
140
|
+
if (isRetry) {
|
|
141
|
+
console.log(chalk.yellow(`\n Retry ${attempt - 1}/${MAX_RETRIES - 1}: fixing validation errors...`));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build prompt — first attempt uses extraction prompt, retries include error feedback
|
|
145
|
+
const activeSystemPrompt = opts.isFrontend ? dslFrontendSystemPrompt : dslSystemPrompt;
|
|
146
|
+
const userPrompt = isRetry
|
|
147
|
+
? buildDslRetryPrompt(specForAI, lastRawOutput, lastErrors)
|
|
148
|
+
: buildDslExtractionPrompt(specForAI, opts.isFrontend);
|
|
149
|
+
|
|
150
|
+
// Call AI
|
|
151
|
+
let rawOutput: string;
|
|
152
|
+
try {
|
|
153
|
+
rawOutput = await this.provider.generate(userPrompt, activeSystemPrompt);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.log(chalk.red(` ✘ AI call failed: ${(err as Error).message}`));
|
|
156
|
+
// Don't retry on network/API errors — ask user immediately
|
|
157
|
+
return this.handleFailure(opts, "AI call failed");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lastRawOutput = rawOutput;
|
|
161
|
+
|
|
162
|
+
// Parse JSON
|
|
163
|
+
let parsed: unknown;
|
|
164
|
+
try {
|
|
165
|
+
parsed = parseJsonFromOutput(rawOutput);
|
|
166
|
+
} catch (parseErr) {
|
|
167
|
+
console.log(chalk.red(` ✘ Failed to parse JSON from AI output: ${(parseErr as Error).message}`));
|
|
168
|
+
lastErrors = [{ path: "root", message: "Output is not valid JSON — see raw output above" }];
|
|
169
|
+
|
|
170
|
+
if (attempt < MAX_RETRIES) continue;
|
|
171
|
+
return this.handleFailure(opts, "AI produced invalid JSON after retries");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate schema
|
|
175
|
+
const result = validateDsl(sanitizeDsl(parsed));
|
|
176
|
+
|
|
177
|
+
if (result.valid) {
|
|
178
|
+
printDslSummary(result.dsl);
|
|
179
|
+
return result.dsl;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validation failed
|
|
183
|
+
printValidationErrors(result.errors);
|
|
184
|
+
lastErrors = result.errors;
|
|
185
|
+
|
|
186
|
+
if (attempt < MAX_RETRIES) {
|
|
187
|
+
console.log(chalk.gray(` Will retry with error feedback...`));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// All retries exhausted
|
|
192
|
+
return this.handleFailure(opts, `DSL validation failed after ${MAX_RETRIES} attempts`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Should be unreachable, but TypeScript needs a return
|
|
196
|
+
return this.handleFailure(opts, "Unexpected extraction loop exit");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* When extraction fails: in --auto mode skip silently; interactively ask user.
|
|
201
|
+
* Returns null to skip, or throws to abort the pipeline.
|
|
202
|
+
*/
|
|
203
|
+
private async handleFailure(
|
|
204
|
+
opts: { auto?: boolean },
|
|
205
|
+
reason: string
|
|
206
|
+
): Promise<null> {
|
|
207
|
+
console.log(chalk.yellow(`\n ⚠ DSL extraction failed: ${reason}`));
|
|
208
|
+
|
|
209
|
+
if (opts.auto) {
|
|
210
|
+
console.log(chalk.gray(" --auto mode: skipping DSL, continuing without it."));
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const action = await select({
|
|
215
|
+
message: "DSL extraction failed. What would you like to do?",
|
|
216
|
+
choices: [
|
|
217
|
+
{ name: "⏭ Skip DSL — continue to code generation without it", value: "skip" },
|
|
218
|
+
{ name: "❌ Abort — stop the pipeline", value: "abort" },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (action === "abort") {
|
|
223
|
+
console.log(chalk.red(" Pipeline aborted by user."));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(chalk.gray(" Continuing without DSL."));
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Save ────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async saveDsl(dsl: SpecDSL, specFilePath: string): Promise<string> {
|
|
234
|
+
const outPath = dslFilePath(specFilePath);
|
|
235
|
+
await fs.writeJson(outPath, dsl, { spaces: 2 });
|
|
236
|
+
return outPath;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── DSL summary for codegen prompts ──────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build a compact, token-efficient DSL summary to inject into codegen prompts.
|
|
244
|
+
* Avoids dumping the full DSL JSON (which would be large) — only extracts
|
|
245
|
+
* the most actionable parts: endpoint signatures and model field lists.
|
|
246
|
+
*/
|
|
247
|
+
export function buildDslContextSection(dsl: SpecDSL): string {
|
|
248
|
+
const lines: string[] = [
|
|
249
|
+
"=== Feature DSL (structured summary — use for implementation guidance) ===",
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
// Models
|
|
253
|
+
if (dsl.models.length > 0) {
|
|
254
|
+
lines.push("\n-- Data Models --");
|
|
255
|
+
for (const model of dsl.models) {
|
|
256
|
+
lines.push(`${model.name}:`);
|
|
257
|
+
for (const field of model.fields) {
|
|
258
|
+
const flags: string[] = [];
|
|
259
|
+
if (field.required) flags.push("required");
|
|
260
|
+
if (field.unique) flags.push("unique");
|
|
261
|
+
lines.push(` ${field.name}: ${field.type}${flags.length ? ` (${flags.join(", ")})` : ""}`);
|
|
262
|
+
}
|
|
263
|
+
if (model.relations && model.relations.length > 0) {
|
|
264
|
+
lines.push(` relations: ${model.relations.join("; ")}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Endpoints
|
|
270
|
+
if (dsl.endpoints.length > 0) {
|
|
271
|
+
lines.push("\n-- API Endpoints --");
|
|
272
|
+
for (const ep of dsl.endpoints) {
|
|
273
|
+
lines.push(`${ep.id}: ${ep.method} ${ep.path} [auth: ${ep.auth}] → ${ep.successStatus}`);
|
|
274
|
+
lines.push(` ${ep.description}`);
|
|
275
|
+
if (ep.request?.body) {
|
|
276
|
+
const fields = Object.entries(ep.request.body)
|
|
277
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
278
|
+
.join(", ");
|
|
279
|
+
lines.push(` body: { ${fields} }`);
|
|
280
|
+
}
|
|
281
|
+
if (ep.errors && ep.errors.length > 0) {
|
|
282
|
+
lines.push(` errors: ${ep.errors.map((e) => `${e.status} ${e.code}`).join(", ")}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Behaviors
|
|
288
|
+
if (dsl.behaviors.length > 0) {
|
|
289
|
+
lines.push("\n-- Business Behaviors --");
|
|
290
|
+
for (const b of dsl.behaviors) {
|
|
291
|
+
lines.push(`${b.id}: ${b.description}`);
|
|
292
|
+
if (b.trigger) lines.push(` trigger: ${b.trigger}`);
|
|
293
|
+
if (b.constraints && b.constraints.length > 0) {
|
|
294
|
+
lines.push(` rules: ${b.constraints.join("; ")}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Components (frontend only)
|
|
300
|
+
if (dsl.components && dsl.components.length > 0) {
|
|
301
|
+
lines.push("\n-- UI Components --");
|
|
302
|
+
for (const cmp of dsl.components) {
|
|
303
|
+
lines.push(`${cmp.id}: ${cmp.name} — ${cmp.description}`);
|
|
304
|
+
if (cmp.props.length > 0) {
|
|
305
|
+
lines.push(` props: ${cmp.props.map((p) => `${p.name}${p.required ? "" : "?"}:${p.type}`).join(", ")}`);
|
|
306
|
+
}
|
|
307
|
+
if (cmp.events.length > 0) {
|
|
308
|
+
lines.push(` events: ${cmp.events.map((e) => `${e.name}(${e.payload ?? ""})`).join(", ")}`);
|
|
309
|
+
}
|
|
310
|
+
if (Object.keys(cmp.state).length > 0) {
|
|
311
|
+
lines.push(` state: ${Object.entries(cmp.state).map(([k, v]) => `${k}:${v}`).join(", ")}`);
|
|
312
|
+
}
|
|
313
|
+
if (cmp.apiCalls.length > 0) {
|
|
314
|
+
lines.push(` calls: ${cmp.apiCalls.join(", ")}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
lines.push("\n=== End of DSL ===");
|
|
320
|
+
return lines.join("\n");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Load DSL from disk if available alongside a spec file.
|
|
325
|
+
* Returns null (never throws) if file is missing or corrupt.
|
|
326
|
+
*/
|
|
327
|
+
export async function loadDslForSpec(specFilePath: string): Promise<SpecDSL | null> {
|
|
328
|
+
const dslPath = dslFilePath(specFilePath);
|
|
329
|
+
if (!(await fs.pathExists(dslPath))) return null;
|
|
330
|
+
try {
|
|
331
|
+
const raw = await fs.readJson(dslPath);
|
|
332
|
+
const result = validateDsl(raw);
|
|
333
|
+
return result.valid ? result.dsl : null;
|
|
334
|
+
} catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecDSL — Structured intermediate representation of a Feature Spec.
|
|
3
|
+
*
|
|
4
|
+
* Design constraints (intentional):
|
|
5
|
+
* - No recursive types: every type is a flat object or a primitive array.
|
|
6
|
+
* - No generics / conditional types: keeps TS compilation simple and fast.
|
|
7
|
+
* - request/response schemas use Record<string,string> (field→type-description)
|
|
8
|
+
* rather than deep nested JSON-Schema objects, to avoid hallucination traps.
|
|
9
|
+
* - All arrays may be empty ([]) — extractor must NOT invent entries.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ─── Leaf types ──────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
15
|
+
|
|
16
|
+
export type DslVersion = "1.0";
|
|
17
|
+
|
|
18
|
+
// ─── Feature metadata ────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface FeatureMeta {
|
|
21
|
+
/** Slugified feature identifier, e.g. "user-login" */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Human-readable title, verbatim from spec heading */
|
|
24
|
+
title: string;
|
|
25
|
+
/** One-paragraph description */
|
|
26
|
+
description: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Data models ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface ModelField {
|
|
32
|
+
name: string;
|
|
33
|
+
/**
|
|
34
|
+
* Primitive or Prisma-style type string:
|
|
35
|
+
* "String" | "Int" | "Float" | "Boolean" | "DateTime" | "Json" | "<ModelName>"
|
|
36
|
+
*/
|
|
37
|
+
type: string;
|
|
38
|
+
required: boolean;
|
|
39
|
+
unique?: boolean;
|
|
40
|
+
description?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DataModel {
|
|
44
|
+
name: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
fields: ModelField[];
|
|
47
|
+
/**
|
|
48
|
+
* Plain-text relation descriptions — NOT nested objects.
|
|
49
|
+
* e.g. ["belongs to User via userId", "has many OrderItem"]
|
|
50
|
+
*/
|
|
51
|
+
relations?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── API endpoints ───────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Flat map of field-name → type-description (string).
|
|
58
|
+
* We deliberately avoid deep JSON-Schema nesting to prevent hallucinations.
|
|
59
|
+
*/
|
|
60
|
+
export type FieldMap = Record<string, string>;
|
|
61
|
+
|
|
62
|
+
export interface RequestSchema {
|
|
63
|
+
body?: FieldMap;
|
|
64
|
+
query?: FieldMap;
|
|
65
|
+
params?: FieldMap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ResponseError {
|
|
69
|
+
status: number;
|
|
70
|
+
code: string;
|
|
71
|
+
description: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ApiEndpoint {
|
|
75
|
+
/** Sequential identifier, e.g. "EP-001" */
|
|
76
|
+
id: string;
|
|
77
|
+
method: HttpMethod;
|
|
78
|
+
/** Must start with "/" */
|
|
79
|
+
path: string;
|
|
80
|
+
description: string;
|
|
81
|
+
/** Whether the endpoint requires authentication */
|
|
82
|
+
auth: boolean;
|
|
83
|
+
request?: RequestSchema;
|
|
84
|
+
successStatus: number;
|
|
85
|
+
successDescription: string;
|
|
86
|
+
errors?: ResponseError[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Business behaviors ──────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface BusinessBehavior {
|
|
92
|
+
/** Sequential identifier, e.g. "BHV-001" */
|
|
93
|
+
id: string;
|
|
94
|
+
description: string;
|
|
95
|
+
/** What event/action triggers this behavior */
|
|
96
|
+
trigger?: string;
|
|
97
|
+
/** Business rules or constraints that apply */
|
|
98
|
+
constraints?: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Frontend component specs ─────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Describes a single UI component that needs to be created or modified.
|
|
105
|
+
* Only populated when the target project is a frontend repo.
|
|
106
|
+
*/
|
|
107
|
+
export interface ComponentProp {
|
|
108
|
+
name: string;
|
|
109
|
+
type: string;
|
|
110
|
+
required: boolean;
|
|
111
|
+
description?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ComponentEvent {
|
|
115
|
+
/** Event name, e.g. "onClick", "onSuccess" */
|
|
116
|
+
name: string;
|
|
117
|
+
/** Payload type description */
|
|
118
|
+
payload?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ComponentSpec {
|
|
122
|
+
/** Sequential identifier, e.g. "CMP-001" */
|
|
123
|
+
id: string;
|
|
124
|
+
/** PascalCase component name */
|
|
125
|
+
name: string;
|
|
126
|
+
description: string;
|
|
127
|
+
/** Props the component accepts */
|
|
128
|
+
props: ComponentProp[];
|
|
129
|
+
/** Events / callbacks the component emits */
|
|
130
|
+
events: ComponentEvent[];
|
|
131
|
+
/** Local state the component manages (name → type description) */
|
|
132
|
+
state: Record<string, string>;
|
|
133
|
+
/** API endpoints this component calls directly */
|
|
134
|
+
apiCalls: string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Root DSL type ───────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export interface SpecDSL {
|
|
140
|
+
version: DslVersion;
|
|
141
|
+
feature: FeatureMeta;
|
|
142
|
+
models: DataModel[];
|
|
143
|
+
endpoints: ApiEndpoint[];
|
|
144
|
+
/**
|
|
145
|
+
* Non-CRUD business behaviors (side effects, rules, async flows).
|
|
146
|
+
* Can be empty — do NOT invent entries if spec doesn't mention them.
|
|
147
|
+
*/
|
|
148
|
+
behaviors: BusinessBehavior[];
|
|
149
|
+
/**
|
|
150
|
+
* Frontend component specs — only present when the feature involves UI.
|
|
151
|
+
* Backend-only specs will have an empty array here.
|
|
152
|
+
*/
|
|
153
|
+
components?: ComponentSpec[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Validation result ────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
export interface DslValidationError {
|
|
159
|
+
/** JSON-pointer style path, e.g. "endpoints[1].method" */
|
|
160
|
+
path: string;
|
|
161
|
+
message: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type DslValidationResult =
|
|
165
|
+
| { valid: true; dsl: SpecDSL }
|
|
166
|
+
| { valid: false; errors: DslValidationError[] };
|