ai-spec-dev 0.46.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -30
- package/cli/commands/config.ts +129 -1
- package/cli/commands/create.ts +14 -0
- package/cli/commands/fix-history.ts +176 -0
- package/cli/commands/init.ts +36 -1
- package/cli/index.ts +2 -6
- package/cli/pipeline/helpers.ts +6 -0
- package/cli/pipeline/multi-repo.ts +300 -26
- package/cli/pipeline/single-repo.ts +103 -2
- package/cli/utils.ts +23 -0
- package/core/code-generator.ts +63 -14
- package/core/cross-stack-verifier.ts +482 -0
- package/core/fix-history.ts +333 -0
- package/core/import-fixer.ts +827 -0
- package/core/import-verifier.ts +569 -0
- package/core/knowledge-memory.ts +55 -6
- package/core/self-evaluator.ts +44 -7
- package/core/spec-generator.ts +3 -3
- package/core/types-generator.ts +2 -2
- package/dist/cli/index.js +3968 -2353
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3810 -2195
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +249 -128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +249 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/cross-stack-verifier.test.ts +402 -0
- package/tests/fix-history.test.ts +335 -0
- package/tests/import-fixer.test.ts +944 -0
- package/tests/import-verifier.test.ts +420 -0
- package/tests/knowledge-memory.test.ts +40 -0
- package/tests/self-evaluator.test.ts +97 -0
- package/.ai-spec-workspace.json +0 -17
- package/.ai-spec.json +0 -7
- package/cli/commands/model.ts +0 -152
- package/cli/commands/scan.ts +0 -99
- package/cli/commands/workspace.ts +0 -219
|
@@ -35,10 +35,33 @@ import { loadFrontendContext } from "../../core/frontend-context-loader";
|
|
|
35
35
|
import { buildFrontendSpecPrompt } from "../../prompts/frontend-spec.prompt";
|
|
36
36
|
import { AiSpecConfig, resolveApiKey } from "../utils";
|
|
37
37
|
import { printBanner, MultiRepoResult } from "./helpers";
|
|
38
|
+
import {
|
|
39
|
+
verifyCrossStackContract,
|
|
40
|
+
printCrossStackReport,
|
|
41
|
+
} from "../../core/cross-stack-verifier";
|
|
42
|
+
import {
|
|
43
|
+
verifyImports,
|
|
44
|
+
printImportVerificationReport,
|
|
45
|
+
} from "../../core/import-verifier";
|
|
46
|
+
import { runImportFix, printFixReport } from "../../core/import-fixer";
|
|
47
|
+
import { generateRunId, RunLogger, setActiveLogger } from "../../core/run-logger";
|
|
38
48
|
import * as fs from "fs-extra";
|
|
39
49
|
|
|
40
50
|
// ─── Single-repo workspace pipeline ──────────────────────────────────────────
|
|
41
51
|
|
|
52
|
+
export interface WorkspaceRepoRunResult {
|
|
53
|
+
/** True only when spec_gen + codegen both succeeded and produced files. */
|
|
54
|
+
success: boolean;
|
|
55
|
+
/** Human-readable reason when success === false. */
|
|
56
|
+
failureReason?: string;
|
|
57
|
+
specFile: string | null;
|
|
58
|
+
dsl: SpecDSL | null;
|
|
59
|
+
/** Files written by codegen. Empty when codegen failed or wrote nothing. */
|
|
60
|
+
generatedFiles: string[];
|
|
61
|
+
/** Per-repo RunLogger ID — usable with `ai-spec logs <runId>` from the repo dir. */
|
|
62
|
+
runId: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
export async function runSingleRepoPipelineInWorkspace(opts: {
|
|
43
66
|
idea: string;
|
|
44
67
|
specProvider: AIProvider;
|
|
@@ -50,7 +73,10 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
50
73
|
repoName: string;
|
|
51
74
|
cliOpts: Record<string, unknown>;
|
|
52
75
|
contractContextSection?: string;
|
|
53
|
-
|
|
76
|
+
maxCodegenConcurrency?: number;
|
|
77
|
+
injectFixHistory?: boolean;
|
|
78
|
+
fixHistoryInjectMax?: number;
|
|
79
|
+
}): Promise<WorkspaceRepoRunResult> {
|
|
54
80
|
const {
|
|
55
81
|
idea,
|
|
56
82
|
specProvider,
|
|
@@ -64,11 +90,22 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
64
90
|
contractContextSection,
|
|
65
91
|
} = opts;
|
|
66
92
|
|
|
93
|
+
// ── Per-repo RunLogger ─────────────────────────────────────────────────────
|
|
94
|
+
// Each repo in a workspace pipeline gets its own log dir under the repo root
|
|
95
|
+
// so users can run `ai-spec logs` from inside the repo to debug failures.
|
|
96
|
+
const runId = generateRunId();
|
|
97
|
+
const runLogger = new RunLogger(repoAbsPath, runId, {
|
|
98
|
+
provider: specProviderName,
|
|
99
|
+
model: specModelName,
|
|
100
|
+
});
|
|
101
|
+
setActiveLogger(runLogger);
|
|
102
|
+
|
|
67
103
|
console.log(chalk.blue(`\n [${repoName}] Loading project context...`));
|
|
104
|
+
runLogger.stageStart("context_load");
|
|
68
105
|
const loader = new ContextLoader(repoAbsPath);
|
|
69
106
|
let context = await loader.loadProjectContext();
|
|
70
|
-
|
|
71
107
|
const { type: detectedRepoType } = await detectRepoType(repoAbsPath);
|
|
108
|
+
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
72
109
|
|
|
73
110
|
console.log(chalk.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
74
111
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
@@ -96,34 +133,55 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
96
133
|
fullIdea = `${idea}\n\n${contractContextSection}`;
|
|
97
134
|
}
|
|
98
135
|
|
|
136
|
+
// ── Spec Generation (CRITICAL: failure here aborts the repo) ───────────────
|
|
99
137
|
console.log(chalk.blue(` [${repoName}] Generating spec...`));
|
|
138
|
+
runLogger.stageStart("spec_gen");
|
|
100
139
|
let finalSpec: string;
|
|
101
140
|
try {
|
|
102
141
|
const result = await generateSpecWithTasks(specProvider, fullIdea, context);
|
|
103
142
|
finalSpec = result.spec;
|
|
143
|
+
runLogger.stageEnd("spec_gen", { specLength: finalSpec.length });
|
|
104
144
|
console.log(chalk.green(` Spec generated.`));
|
|
105
145
|
} catch (err) {
|
|
106
|
-
|
|
107
|
-
|
|
146
|
+
const msg = (err as Error).message;
|
|
147
|
+
runLogger.stageFail("spec_gen", msg);
|
|
148
|
+
runLogger.finish();
|
|
149
|
+
console.error(chalk.red(` ✘ Spec generation failed: ${msg}`));
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
failureReason: `spec_gen failed: ${msg}`,
|
|
153
|
+
specFile: null,
|
|
154
|
+
dsl: null,
|
|
155
|
+
generatedFiles: [],
|
|
156
|
+
runId,
|
|
157
|
+
};
|
|
108
158
|
}
|
|
109
159
|
|
|
110
|
-
// DSL Extraction
|
|
160
|
+
// ── DSL Extraction (non-fatal if missing) ──────────────────────────────────
|
|
111
161
|
let extractedDsl: SpecDSL | null = null;
|
|
112
162
|
if (!cliOpts.skipDsl) {
|
|
113
163
|
console.log(chalk.blue(` [${repoName}] Extracting DSL...`));
|
|
164
|
+
runLogger.stageStart("dsl_extract");
|
|
114
165
|
try {
|
|
115
166
|
const dslExtractor = new DslExtractor(specProvider);
|
|
116
167
|
const repoIsFrontend = isFrontendDeps(context.dependencies);
|
|
117
168
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: true, isFrontend: repoIsFrontend });
|
|
118
169
|
if (extractedDsl) {
|
|
170
|
+
runLogger.stageEnd("dsl_extract", {
|
|
171
|
+
endpoints: extractedDsl.endpoints?.length ?? 0,
|
|
172
|
+
models: extractedDsl.models?.length ?? 0,
|
|
173
|
+
});
|
|
119
174
|
console.log(chalk.green(` DSL extracted.`));
|
|
175
|
+
} else {
|
|
176
|
+
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
120
177
|
}
|
|
121
178
|
} catch (err) {
|
|
179
|
+
runLogger.stageFail("dsl_extract", (err as Error).message);
|
|
122
180
|
console.log(chalk.yellow(` DSL extraction failed: ${(err as Error).message}`));
|
|
123
181
|
}
|
|
124
182
|
}
|
|
125
183
|
|
|
126
|
-
// Git Worktree
|
|
184
|
+
// ── Git Worktree (auto-skip for frontend repos) ────────────────────────────
|
|
127
185
|
const isFrontendRepo = isFrontendDeps(context.dependencies ?? []);
|
|
128
186
|
const skipWorktreeForRepo = cliOpts.worktree
|
|
129
187
|
? false
|
|
@@ -143,7 +201,7 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
143
201
|
console.log(chalk.gray(` [${repoName}] Skipping worktree${isFrontendRepo ? " (frontend repo)" : ""}.`));
|
|
144
202
|
}
|
|
145
203
|
|
|
146
|
-
// Save Spec
|
|
204
|
+
// ── Save Spec ──────────────────────────────────────────────────────────────
|
|
147
205
|
const specsDir = path.join(workingDir, "specs");
|
|
148
206
|
await fs.ensureDir(specsDir);
|
|
149
207
|
const featureSlug = slugify(idea);
|
|
@@ -158,55 +216,158 @@ export async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
158
216
|
console.log(chalk.green(` DSL saved: ${path.relative(repoAbsPath, savedDslFile)}`));
|
|
159
217
|
}
|
|
160
218
|
|
|
161
|
-
// Code Generation
|
|
219
|
+
// ── Code Generation (CRITICAL: failure or 0 files = repo failed) ───────────
|
|
162
220
|
console.log(chalk.blue(` [${repoName}] Running code generation (mode: ${codegenMode})...`));
|
|
221
|
+
runLogger.stageStart("codegen", { mode: codegenMode });
|
|
222
|
+
let generatedFiles: string[] = [];
|
|
163
223
|
try {
|
|
164
224
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
165
|
-
await codegen.generateCode(specFile, workingDir, context, {
|
|
225
|
+
generatedFiles = await codegen.generateCode(specFile, workingDir, context, {
|
|
166
226
|
auto: true,
|
|
167
227
|
dslFilePath: savedDslFile ?? undefined,
|
|
168
228
|
repoType: detectedRepoType,
|
|
229
|
+
maxConcurrency: opts.maxCodegenConcurrency,
|
|
230
|
+
injectFixHistory: opts.injectFixHistory,
|
|
231
|
+
fixHistoryInjectMax: opts.fixHistoryInjectMax,
|
|
169
232
|
});
|
|
170
|
-
|
|
233
|
+
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
171
234
|
} catch (err) {
|
|
172
|
-
|
|
235
|
+
const msg = (err as Error).message;
|
|
236
|
+
runLogger.stageFail("codegen", msg);
|
|
237
|
+
runLogger.finish();
|
|
238
|
+
console.error(chalk.red(` ✘ Code generation failed: ${msg}`));
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
failureReason: `codegen failed: ${msg}`,
|
|
242
|
+
specFile,
|
|
243
|
+
dsl: extractedDsl,
|
|
244
|
+
generatedFiles: [],
|
|
245
|
+
runId,
|
|
246
|
+
};
|
|
173
247
|
}
|
|
174
248
|
|
|
175
|
-
//
|
|
249
|
+
// claude-code mode returns [] by design (the claude CLI writes files itself).
|
|
250
|
+
// Only treat empty as failure when we're in api mode where we expect a list.
|
|
251
|
+
if (generatedFiles.length === 0 && codegenMode === "api") {
|
|
252
|
+
const msg = "Code generation produced 0 files (likely planning step returned empty filePlan)";
|
|
253
|
+
runLogger.stageFail("codegen", msg);
|
|
254
|
+
runLogger.finish();
|
|
255
|
+
console.error(chalk.red(` ✘ ${msg}`));
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
failureReason: msg,
|
|
259
|
+
specFile,
|
|
260
|
+
dsl: extractedDsl,
|
|
261
|
+
generatedFiles: [],
|
|
262
|
+
runId,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log(chalk.green(` Code generation complete (${generatedFiles.length} file(s)).`));
|
|
267
|
+
|
|
268
|
+
// ── Import Verification + Auto-Fix ─────────────────────────────────────────
|
|
269
|
+
// Same two-stage repair flow as single-repo: deterministic DSL stubs + AI fallback.
|
|
270
|
+
if (generatedFiles.length > 0) {
|
|
271
|
+
runLogger.stageStart("import_verify");
|
|
272
|
+
try {
|
|
273
|
+
const absFiles = generatedFiles.map((f) =>
|
|
274
|
+
path.isAbsolute(f) ? f : path.join(workingDir, f)
|
|
275
|
+
);
|
|
276
|
+
const importReport = await verifyImports(absFiles, workingDir);
|
|
277
|
+
printImportVerificationReport(repoName, importReport);
|
|
278
|
+
runLogger.stageEnd("import_verify", {
|
|
279
|
+
totalImports: importReport.totalImports,
|
|
280
|
+
broken: importReport.brokenImports.length,
|
|
281
|
+
external: importReport.externalImports,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (importReport.brokenImports.length > 0) {
|
|
285
|
+
runLogger.stageStart("import_fix");
|
|
286
|
+
try {
|
|
287
|
+
const fixReport = await runImportFix({
|
|
288
|
+
brokenImports: importReport.brokenImports,
|
|
289
|
+
dsl: extractedDsl,
|
|
290
|
+
repoRoot: workingDir,
|
|
291
|
+
generatedFilePaths: absFiles,
|
|
292
|
+
provider: codegenProvider,
|
|
293
|
+
runId,
|
|
294
|
+
recordHistory: true,
|
|
295
|
+
});
|
|
296
|
+
printFixReport(repoName, fixReport);
|
|
297
|
+
runLogger.stageEnd("import_fix", {
|
|
298
|
+
deterministic: fixReport.deterministicCount,
|
|
299
|
+
aiFixed: fixReport.aiFixedCount,
|
|
300
|
+
applied: fixReport.applied.length,
|
|
301
|
+
unresolved: fixReport.unresolvedCount,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (fixReport.applied.length > 0) {
|
|
305
|
+
console.log(chalk.blue(`\n Re-running import verifier after fixes...`));
|
|
306
|
+
const reverifyReport = await verifyImports(absFiles, workingDir);
|
|
307
|
+
printImportVerificationReport(`${repoName} (after fix)`, reverifyReport);
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
runLogger.stageFail("import_fix", (err as Error).message);
|
|
311
|
+
console.log(chalk.yellow(` Import auto-fix failed: ${(err as Error).message}`));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
runLogger.stageFail("import_verify", (err as Error).message);
|
|
316
|
+
console.log(chalk.yellow(` Import verification failed: ${(err as Error).message}`));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Test Generation (non-fatal) ────────────────────────────────────────────
|
|
176
321
|
if (!cliOpts.skipTests && extractedDsl) {
|
|
177
322
|
console.log(chalk.blue(` [${repoName}] Generating test skeletons...`));
|
|
323
|
+
runLogger.stageStart("test_gen");
|
|
178
324
|
try {
|
|
179
325
|
const testGen = new TestGenerator(codegenProvider);
|
|
180
326
|
const testFiles = await testGen.generate(extractedDsl, workingDir);
|
|
327
|
+
runLogger.stageEnd("test_gen", { testFiles: testFiles.length });
|
|
181
328
|
console.log(chalk.green(` ${testFiles.length} test file(s) generated.`));
|
|
182
329
|
} catch (err) {
|
|
330
|
+
runLogger.stageFail("test_gen", (err as Error).message);
|
|
183
331
|
console.log(chalk.yellow(` Test generation failed: ${(err as Error).message}`));
|
|
184
332
|
}
|
|
185
333
|
}
|
|
186
334
|
|
|
187
|
-
// Error Feedback
|
|
335
|
+
// ── Error Feedback (non-fatal) ─────────────────────────────────────────────
|
|
188
336
|
if (!cliOpts.skipErrorFeedback) {
|
|
337
|
+
runLogger.stageStart("error_feedback");
|
|
189
338
|
try {
|
|
190
339
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, { maxCycles: 1 });
|
|
340
|
+
runLogger.stageEnd("error_feedback");
|
|
191
341
|
} catch (err) {
|
|
342
|
+
runLogger.stageFail("error_feedback", (err as Error).message);
|
|
192
343
|
console.log(chalk.yellow(` Error feedback failed: ${(err as Error).message}`));
|
|
193
344
|
}
|
|
194
345
|
}
|
|
195
346
|
|
|
196
|
-
// Code Review
|
|
347
|
+
// ── Code Review (non-fatal) ────────────────────────────────────────────────
|
|
197
348
|
if (!cliOpts.skipReview) {
|
|
198
349
|
console.log(chalk.blue(` [${repoName}] Running code review...`));
|
|
350
|
+
runLogger.stageStart("review");
|
|
199
351
|
try {
|
|
200
352
|
const reviewer = new CodeReviewer(specProvider, workingDir);
|
|
201
353
|
const reviewResult = await reviewer.reviewCode(finalSpec);
|
|
202
354
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
355
|
+
runLogger.stageEnd("review");
|
|
203
356
|
console.log(chalk.green(` Code review complete.`));
|
|
204
357
|
} catch (err) {
|
|
358
|
+
runLogger.stageFail("review", (err as Error).message);
|
|
205
359
|
console.log(chalk.yellow(` Code review failed: ${(err as Error).message}`));
|
|
206
360
|
}
|
|
207
361
|
}
|
|
208
362
|
|
|
209
|
-
|
|
363
|
+
runLogger.finish();
|
|
364
|
+
return {
|
|
365
|
+
success: true,
|
|
366
|
+
specFile,
|
|
367
|
+
dsl: extractedDsl,
|
|
368
|
+
generatedFiles,
|
|
369
|
+
runId,
|
|
370
|
+
};
|
|
210
371
|
}
|
|
211
372
|
|
|
212
373
|
// ─── Multi-repo pipeline ────────────────────────────────────────────────────
|
|
@@ -227,7 +388,7 @@ export async function runMultiRepoPipeline(
|
|
|
227
388
|
const specApiKey = await resolveApiKey(specProviderName, opts.key as string | undefined);
|
|
228
389
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
229
390
|
|
|
230
|
-
const codegenMode: CodeGenMode = ((opts.codegen as string) as CodeGenMode) || config.codegen || "
|
|
391
|
+
const codegenMode: CodeGenMode = ((opts.codegen as string) as CodeGenMode) || config.codegen || "api";
|
|
231
392
|
const codegenProviderName = (opts.codegenProvider as string) || config.codegenProvider || specProviderName;
|
|
232
393
|
const codegenModelName = (opts.codegenModel as string) || config.codegenModel || DEFAULT_MODELS[codegenProviderName];
|
|
233
394
|
const codegenApiKey =
|
|
@@ -400,7 +561,7 @@ export async function runMultiRepoPipeline(
|
|
|
400
561
|
}
|
|
401
562
|
|
|
402
563
|
try {
|
|
403
|
-
const
|
|
564
|
+
const repoResult = await runSingleRepoPipelineInWorkspace({
|
|
404
565
|
idea: specIdea,
|
|
405
566
|
specProvider,
|
|
406
567
|
specProviderName,
|
|
@@ -411,30 +572,143 @@ export async function runMultiRepoPipeline(
|
|
|
411
572
|
repoName: repoReq.repoName,
|
|
412
573
|
cliOpts: opts,
|
|
413
574
|
contractContextSection,
|
|
575
|
+
maxCodegenConcurrency: config.maxCodegenConcurrency,
|
|
576
|
+
injectFixHistory: config.injectFixHistory,
|
|
577
|
+
fixHistoryInjectMax: config.fixHistoryInjectMax,
|
|
414
578
|
});
|
|
415
579
|
|
|
416
|
-
if (repoReq.isContractProvider && dsl) {
|
|
417
|
-
contractDsls.set(repoReq.repoName, dsl);
|
|
580
|
+
if (repoResult.success && repoReq.isContractProvider && repoResult.dsl) {
|
|
581
|
+
contractDsls.set(repoReq.repoName, repoResult.dsl);
|
|
418
582
|
console.log(chalk.green(` Contract stored for downstream repos.`));
|
|
419
583
|
}
|
|
420
584
|
|
|
421
|
-
|
|
422
|
-
|
|
585
|
+
if (repoResult.success) {
|
|
586
|
+
results.push({
|
|
587
|
+
repoName: repoReq.repoName,
|
|
588
|
+
status: "success",
|
|
589
|
+
specFile: repoResult.specFile,
|
|
590
|
+
dsl: repoResult.dsl,
|
|
591
|
+
repoAbsPath,
|
|
592
|
+
role: repoReq.role,
|
|
593
|
+
generatedFiles: repoResult.generatedFiles,
|
|
594
|
+
runId: repoResult.runId,
|
|
595
|
+
});
|
|
596
|
+
console.log(chalk.green(` ✔ ${repoReq.repoName} complete (${repoResult.generatedFiles.length} files, runId: ${repoResult.runId})`));
|
|
597
|
+
} else {
|
|
598
|
+
results.push({
|
|
599
|
+
repoName: repoReq.repoName,
|
|
600
|
+
status: "failed",
|
|
601
|
+
specFile: repoResult.specFile,
|
|
602
|
+
dsl: repoResult.dsl,
|
|
603
|
+
repoAbsPath,
|
|
604
|
+
role: repoReq.role,
|
|
605
|
+
generatedFiles: [],
|
|
606
|
+
failureReason: repoResult.failureReason,
|
|
607
|
+
runId: repoResult.runId,
|
|
608
|
+
});
|
|
609
|
+
console.error(chalk.red(` ✘ ${repoReq.repoName} failed: ${repoResult.failureReason ?? "unknown error"}`));
|
|
610
|
+
console.error(chalk.gray(` debug: cd ${repoAbsPath} && ai-spec logs ${repoResult.runId}`));
|
|
611
|
+
}
|
|
423
612
|
} catch (err) {
|
|
424
|
-
|
|
425
|
-
|
|
613
|
+
// Unexpected exception (not a stage-level failure caught inside the workspace pipeline).
|
|
614
|
+
console.error(chalk.red(` ✘ ${repoReq.repoName} failed unexpectedly: ${(err as Error).message}`));
|
|
615
|
+
results.push({
|
|
616
|
+
repoName: repoReq.repoName,
|
|
617
|
+
status: "failed",
|
|
618
|
+
specFile: null,
|
|
619
|
+
dsl: null,
|
|
620
|
+
repoAbsPath,
|
|
621
|
+
role: repoReq.role,
|
|
622
|
+
generatedFiles: [],
|
|
623
|
+
failureReason: `unexpected exception: ${(err as Error).message}`,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── Step W5: Cross-stack contract verification ────────────────────────────
|
|
629
|
+
// Verify only repos that actually produced files. Skipping repos that failed
|
|
630
|
+
// earlier prevents the verifier from scanning pre-existing code and producing
|
|
631
|
+
// misleading "phantom endpoints" reports.
|
|
632
|
+
const backendWithDsl = results.find(
|
|
633
|
+
(r) => r.role === "backend" && r.status === "success" && r.dsl && r.generatedFiles.length > 0
|
|
634
|
+
);
|
|
635
|
+
const frontendCandidates = results.filter(
|
|
636
|
+
(r) => r.role === "frontend" || r.role === "mobile"
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
if (backendWithDsl && backendWithDsl.dsl && frontendCandidates.length > 0) {
|
|
640
|
+
console.log(chalk.blue("\n[W5] Cross-stack contract verification..."));
|
|
641
|
+
for (const fe of frontendCandidates) {
|
|
642
|
+
if (fe.status !== "success") {
|
|
643
|
+
console.log(chalk.gray(
|
|
644
|
+
` ⊘ Skipped ${fe.repoName}: repo failed earlier (${fe.failureReason ?? "unknown"})`
|
|
645
|
+
));
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (fe.generatedFiles.length === 0) {
|
|
649
|
+
console.log(chalk.gray(
|
|
650
|
+
` ⊘ Skipped ${fe.repoName}: codegen produced 0 files — nothing to verify`
|
|
651
|
+
));
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
// Scope verification to files generated in THIS run. Without this,
|
|
656
|
+
// the verifier scans the entire frontend repo and reports historical
|
|
657
|
+
// API calls (unrelated to the current feature) as "phantom endpoints",
|
|
658
|
+
// producing misleading output in mature codebases.
|
|
659
|
+
const report = await verifyCrossStackContract(
|
|
660
|
+
backendWithDsl.dsl,
|
|
661
|
+
fe.repoAbsPath,
|
|
662
|
+
{ scopedFiles: fe.generatedFiles }
|
|
663
|
+
);
|
|
664
|
+
printCrossStackReport(fe.repoName, report);
|
|
665
|
+
if (report.hasViolations) {
|
|
666
|
+
console.log(
|
|
667
|
+
chalk.yellow(
|
|
668
|
+
` ⚠ [W5] ${fe.repoName} has cross-stack violations` +
|
|
669
|
+
` (${report.phantom.length} phantom, ${report.methodMismatch.length} method mismatch).` +
|
|
670
|
+
` Review the report above and fix generated frontend code.`
|
|
671
|
+
)
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
} catch (err) {
|
|
675
|
+
console.log(chalk.yellow(` ⚠ Verification failed for ${fe.repoName}: ${(err as Error).message}`));
|
|
676
|
+
}
|
|
426
677
|
}
|
|
678
|
+
} else if (frontendCandidates.length > 0 && !backendWithDsl) {
|
|
679
|
+
console.log(chalk.gray(
|
|
680
|
+
"\n[W5] Cross-stack verification skipped: no backend repo produced a usable DSL in this run."
|
|
681
|
+
));
|
|
427
682
|
}
|
|
428
683
|
|
|
429
684
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
430
|
-
|
|
685
|
+
const successCount = results.filter((r) => r.status === "success").length;
|
|
686
|
+
const failedCount = results.filter((r) => r.status === "failed").length;
|
|
687
|
+
const overallOk = failedCount === 0;
|
|
688
|
+
|
|
689
|
+
if (overallOk) {
|
|
690
|
+
console.log(chalk.bold.green("\n✔ Multi-repo pipeline complete!"));
|
|
691
|
+
} else {
|
|
692
|
+
console.log(chalk.bold.yellow(`\n⚠ Multi-repo pipeline finished with ${failedCount} failure(s).`));
|
|
693
|
+
}
|
|
431
694
|
console.log(chalk.gray(` Workspace: ${workspace.name}`));
|
|
432
695
|
console.log(chalk.gray(` Requirement: ${idea}`));
|
|
696
|
+
console.log(chalk.gray(` Result: ${successCount} success / ${failedCount} failed / ${results.length} total`));
|
|
433
697
|
console.log();
|
|
434
698
|
for (const r of results) {
|
|
435
699
|
const icon = r.status === "success" ? chalk.green("✔") : r.status === "failed" ? chalk.red("✘") : chalk.gray("−");
|
|
436
|
-
|
|
437
|
-
|
|
700
|
+
if (r.status === "success") {
|
|
701
|
+
const fileInfo = chalk.gray(` (${r.generatedFiles.length} files)`);
|
|
702
|
+
const specInfo = r.specFile ? chalk.gray(` → ${r.specFile}`) : "";
|
|
703
|
+
console.log(` ${icon} ${r.repoName}${fileInfo}${specInfo}`);
|
|
704
|
+
} else if (r.status === "failed") {
|
|
705
|
+
console.log(` ${icon} ${chalk.red(r.repoName)} — ${chalk.red(r.failureReason ?? "unknown reason")}`);
|
|
706
|
+
if (r.runId) {
|
|
707
|
+
console.log(chalk.gray(` debug: cd ${r.repoAbsPath} && ai-spec logs ${r.runId}`));
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
console.log(` ${icon} ${r.repoName} (${r.status})`);
|
|
711
|
+
}
|
|
438
712
|
}
|
|
439
713
|
|
|
440
714
|
return results;
|
|
@@ -52,6 +52,12 @@ import {
|
|
|
52
52
|
} from "../../core/vcr";
|
|
53
53
|
import { printBanner } from "./helpers";
|
|
54
54
|
import { startSpinner, startStage } from "../../core/cli-ui";
|
|
55
|
+
import { exportOpenApi } from "../../core/openapi-exporter";
|
|
56
|
+
import { saveTypescriptTypes } from "../../core/types-generator";
|
|
57
|
+
import { input } from "@inquirer/prompts";
|
|
58
|
+
import { appendDirectLesson } from "../../core/knowledge-memory";
|
|
59
|
+
import { verifyImports, printImportVerificationReport } from "../../core/import-verifier";
|
|
60
|
+
import { runImportFix, printFixReport } from "../../core/import-fixer";
|
|
55
61
|
|
|
56
62
|
// ─── Pipeline Options ────────────────────────────────────────────────────────
|
|
57
63
|
|
|
@@ -78,6 +84,8 @@ export interface SingleRepoPipelineOpts {
|
|
|
78
84
|
worktree?: boolean;
|
|
79
85
|
vcrRecord?: boolean;
|
|
80
86
|
vcrReplay?: string;
|
|
87
|
+
openapi?: boolean;
|
|
88
|
+
types?: boolean;
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// ─── Single-repo pipeline ────────────────────────────────────────────────────
|
|
@@ -96,7 +104,7 @@ export async function runSingleRepoPipeline(
|
|
|
96
104
|
|
|
97
105
|
// ── Resolve codegen ─────────────────────────────────────────────────────
|
|
98
106
|
const codegenMode: CodeGenMode =
|
|
99
|
-
(opts.codegen as CodeGenMode) || config.codegen || "
|
|
107
|
+
(opts.codegen as CodeGenMode) || config.codegen || "api";
|
|
100
108
|
const codegenProviderName =
|
|
101
109
|
opts.codegenProvider || config.codegenProvider || specProviderName;
|
|
102
110
|
const codegenModelName =
|
|
@@ -155,7 +163,7 @@ export async function runSingleRepoPipeline(
|
|
|
155
163
|
runLogger.stageStart("context_load");
|
|
156
164
|
const loader = new ContextLoader(currentDir);
|
|
157
165
|
const context = await loader.loadProjectContext();
|
|
158
|
-
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
166
|
+
const { type: detectedRepoType, role: detectedRepoRole } = await detectRepoType(currentDir);
|
|
159
167
|
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
160
168
|
console.log(chalk.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
161
169
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
@@ -498,6 +506,24 @@ export async function runSingleRepoPipeline(
|
|
|
498
506
|
const dslExtractor = new DslExtractor(specProvider);
|
|
499
507
|
savedDslFile = await dslExtractor.saveDsl(extractedDsl, specFile);
|
|
500
508
|
console.log(chalk.green(` ✔ DSL saved : ${savedDslFile}`));
|
|
509
|
+
|
|
510
|
+
// ── Auto-generate OpenAPI / TypeScript types if requested ──────────────
|
|
511
|
+
if (opts.openapi) {
|
|
512
|
+
try {
|
|
513
|
+
const openapiPath = await exportOpenApi(extractedDsl, currentDir, { format: "yaml" });
|
|
514
|
+
console.log(chalk.green(` ✔ OpenAPI : ${path.relative(currentDir, openapiPath)}`));
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.log(chalk.yellow(` ⚠ OpenAPI export failed: ${(err as Error).message}`));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (opts.types) {
|
|
520
|
+
try {
|
|
521
|
+
const typesPath = await saveTypescriptTypes(extractedDsl, currentDir, {});
|
|
522
|
+
console.log(chalk.green(` ✔ TS Types : ${path.relative(currentDir, typesPath)}`));
|
|
523
|
+
} catch (err) {
|
|
524
|
+
console.log(chalk.yellow(` ⚠ Types generation failed: ${(err as Error).message}`));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
501
527
|
}
|
|
502
528
|
|
|
503
529
|
if (!opts.skipTasks) {
|
|
@@ -554,9 +580,70 @@ export async function runSingleRepoPipeline(
|
|
|
554
580
|
resume: opts.resume,
|
|
555
581
|
dslFilePath: savedDslFile ?? undefined,
|
|
556
582
|
repoType: detectedRepoType,
|
|
583
|
+
maxConcurrency: config.maxCodegenConcurrency,
|
|
584
|
+
injectFixHistory: config.injectFixHistory,
|
|
585
|
+
fixHistoryInjectMax: config.fixHistoryInjectMax,
|
|
557
586
|
});
|
|
558
587
|
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
559
588
|
|
|
589
|
+
// ── Step 6.5: Import Verification + Auto-Fix ───────────────────────────
|
|
590
|
+
// Static check that every import in the generated files actually resolves.
|
|
591
|
+
// If broken imports are found, run a two-stage fix:
|
|
592
|
+
// Stage A: deterministic DSL-driven stub generation
|
|
593
|
+
// Stage B: AI-driven repair (only for what Stage A could not handle)
|
|
594
|
+
// After fixes, re-verify to confirm.
|
|
595
|
+
if (generatedFiles.length > 0) {
|
|
596
|
+
runLogger.stageStart("import_verify");
|
|
597
|
+
try {
|
|
598
|
+
const absFiles = generatedFiles.map((f) =>
|
|
599
|
+
path.isAbsolute(f) ? f : path.join(workingDir, f)
|
|
600
|
+
);
|
|
601
|
+
const importReport = await verifyImports(absFiles, workingDir);
|
|
602
|
+
printImportVerificationReport(path.basename(workingDir), importReport);
|
|
603
|
+
runLogger.stageEnd("import_verify", {
|
|
604
|
+
totalImports: importReport.totalImports,
|
|
605
|
+
broken: importReport.brokenImports.length,
|
|
606
|
+
external: importReport.externalImports,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// ── Auto-fix loop ────────────────────────────────────────────────────
|
|
610
|
+
if (importReport.brokenImports.length > 0) {
|
|
611
|
+
runLogger.stageStart("import_fix");
|
|
612
|
+
try {
|
|
613
|
+
const fixReport = await runImportFix({
|
|
614
|
+
brokenImports: importReport.brokenImports,
|
|
615
|
+
dsl: extractedDsl,
|
|
616
|
+
repoRoot: workingDir,
|
|
617
|
+
generatedFilePaths: absFiles,
|
|
618
|
+
provider: codegenProvider,
|
|
619
|
+
runId,
|
|
620
|
+
recordHistory: true,
|
|
621
|
+
});
|
|
622
|
+
printFixReport(path.basename(workingDir), fixReport);
|
|
623
|
+
runLogger.stageEnd("import_fix", {
|
|
624
|
+
deterministic: fixReport.deterministicCount,
|
|
625
|
+
aiFixed: fixReport.aiFixedCount,
|
|
626
|
+
applied: fixReport.applied.length,
|
|
627
|
+
unresolved: fixReport.unresolvedCount,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Re-verify after fixes
|
|
631
|
+
if (fixReport.applied.length > 0) {
|
|
632
|
+
console.log(chalk.blue("\n Re-running import verifier after fixes..."));
|
|
633
|
+
const reverifyReport = await verifyImports(absFiles, workingDir);
|
|
634
|
+
printImportVerificationReport(`${path.basename(workingDir)} (after fix)`, reverifyReport);
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
runLogger.stageFail("import_fix", (err as Error).message);
|
|
638
|
+
console.log(chalk.yellow(` ⚠ Import auto-fix failed: ${(err as Error).message}`));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
runLogger.stageFail("import_verify", (err as Error).message);
|
|
643
|
+
console.log(chalk.yellow(` ⚠ Import verification failed: ${(err as Error).message}`));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
560
647
|
// ── Step 7: Test Skeleton Generation ───────────────────────────────────
|
|
561
648
|
if (opts.tdd) {
|
|
562
649
|
console.log(chalk.gray("\n[7/9] TDD mode — test files already written pre-implementation."));
|
|
@@ -710,6 +797,7 @@ export async function runSingleRepoPipeline(
|
|
|
710
797
|
reviewText: reviewResult,
|
|
711
798
|
promptHash,
|
|
712
799
|
logger: runLogger,
|
|
800
|
+
repoType: detectedRepoRole,
|
|
713
801
|
});
|
|
714
802
|
printSelfEval(selfEvalResult);
|
|
715
803
|
|
|
@@ -745,6 +833,19 @@ export async function runSingleRepoPipeline(
|
|
|
745
833
|
console.log(chalk.yellow(" The pipeline structure may have changed since the recording was made."));
|
|
746
834
|
}
|
|
747
835
|
|
|
836
|
+
// ── Quick lesson capture (skip in auto/fast mode) ───────────────────────
|
|
837
|
+
if (!opts.auto && !opts.fast) {
|
|
838
|
+
try {
|
|
839
|
+
const lesson = await input({ message: "Any lessons to note? (Enter to skip):" });
|
|
840
|
+
if (lesson.trim()) {
|
|
841
|
+
await appendDirectLesson(currentDir, lesson.trim());
|
|
842
|
+
console.log(chalk.green(" ✔ Lesson saved to constitution §9."));
|
|
843
|
+
}
|
|
844
|
+
} catch {
|
|
845
|
+
// non-blocking if prompt is interrupted
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
748
849
|
// ── Done ────────────────────────────────────────────────────────────────
|
|
749
850
|
runLogger.finish();
|
|
750
851
|
console.log(chalk.bold.green("\n✔ All done!"));
|
package/cli/utils.ts
CHANGED
|
@@ -26,6 +26,29 @@ export interface AiSpecConfig extends AiSpecGlobalConfig {
|
|
|
26
26
|
minHarnessScore?: number;
|
|
27
27
|
/** Maximum error-feedback cycles before giving up (default: 2, TDD default: 3). */
|
|
28
28
|
maxErrorCycles?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Maximum number of tasks that can run concurrently within a codegen batch
|
|
31
|
+
* (api mode only). Prevents rate-limit errors when a layer has many independent
|
|
32
|
+
* tasks. Default: 3.
|
|
33
|
+
*/
|
|
34
|
+
maxCodegenConcurrency?: number;
|
|
35
|
+
/**
|
|
36
|
+
* When true (default), past hallucination patterns from
|
|
37
|
+
* `.ai-spec-fix-history.json` are injected into codegen prompts.
|
|
38
|
+
* Set to false to disable automatic learning from fix history.
|
|
39
|
+
*/
|
|
40
|
+
injectFixHistory?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Number of times a hallucination pattern must repeat in fix-history before
|
|
43
|
+
* `ai-spec fix-history --promote` offers it as a constitution §9 lesson.
|
|
44
|
+
* Default: 5.
|
|
45
|
+
*/
|
|
46
|
+
fixHistoryPromotionThreshold?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Maximum number of past hallucination patterns injected into a single
|
|
49
|
+
* codegen prompt. Prevents prompt bloat. Default: 10.
|
|
50
|
+
*/
|
|
51
|
+
fixHistoryInjectMax?: number;
|
|
29
52
|
/** §9 lesson count threshold for auto-consolidation (default: 12). */
|
|
30
53
|
autoConsolidateThreshold?: number;
|
|
31
54
|
|