ai-spec-dev 0.41.0 → 0.42.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/.ai-spec-workspace.json +17 -0
- package/.ai-spec.json +7 -0
- package/cli/pipeline/single-repo.ts +19 -10
- package/core/cli-ui.ts +136 -0
- package/core/code-generator.ts +4 -2
- package/core/error-feedback.ts +4 -2
- package/core/provider-utils.ts +8 -7
- package/demo-backend/.ai-spec-constitution.md +65 -0
- package/demo-backend/package.json +21 -0
- package/demo-backend/prisma/schema.prisma +22 -0
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +186 -0
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +211 -0
- package/demo-backend/src/controllers/bookmark.controller.test.ts +255 -0
- package/demo-backend/src/controllers/bookmark.controller.ts +187 -0
- package/demo-backend/src/index.ts +17 -0
- package/demo-backend/src/routes/bookmark.routes.test.ts +264 -0
- package/demo-backend/src/routes/bookmark.routes.ts +11 -0
- package/demo-backend/src/routes/index.ts +8 -0
- package/demo-backend/src/services/bookmark.service.test.ts +433 -0
- package/demo-backend/src/services/bookmark.service.ts +261 -0
- package/demo-backend/tsconfig.json +12 -0
- package/demo-frontend/.ai-spec-constitution.md +95 -0
- package/demo-frontend/package.json +23 -0
- package/demo-frontend/src/App.tsx +12 -0
- package/demo-frontend/src/main.tsx +9 -0
- package/demo-frontend/tsconfig.json +13 -0
- package/dist/cli/index.js +130 -21
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +130 -21
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js +80 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/RELEASE_LOG.md +0 -2962
- package/purpose.md +0 -1434
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Project Constitution
|
|
2
|
+
|
|
3
|
+
## 1. 架构规则 (Architecture Rules)
|
|
4
|
+
- **组件分层架构**:页面/组件 → 自定义hooks → 服务层 → API客户端
|
|
5
|
+
- **组件组织**:页面组件位于 `src/pages/`,可复用组件位于 `src/components/`
|
|
6
|
+
- **业务逻辑分离**:所有API调用、数据转换和业务逻辑必须封装在自定义hooks或服务层中
|
|
7
|
+
- **状态管理**:使用React Context或本地状态,禁止直接修改props
|
|
8
|
+
- **类型安全**:所有组件props、API响应和状态必须明确定义TypeScript类型
|
|
9
|
+
|
|
10
|
+
## 2. 命名规范 (Naming Conventions)
|
|
11
|
+
- **文件命名**:
|
|
12
|
+
- 组件文件:PascalCase (`UserProfile.tsx`)
|
|
13
|
+
- Hook文件:camelCase,以`use`开头 (`useAuth.ts`)
|
|
14
|
+
- 工具/服务文件:camelCase (`apiService.ts`)
|
|
15
|
+
- 样式文件:与组件同名,使用CSS模块 (`UserProfile.module.css`)
|
|
16
|
+
- **变量/函数**:
|
|
17
|
+
- 变量/函数:camelCase (`userData`, `fetchUserProfile`)
|
|
18
|
+
- 接口/类型:PascalCase (`UserData`, `ApiResponse`)
|
|
19
|
+
- 常量:UPPER_SNAKE_CASE (`API_BASE_URL`)
|
|
20
|
+
- **组件命名**:PascalCase (`<UserProfile />`)
|
|
21
|
+
- **路由路径**:kebab-case (`/user-profile`, `/admin/dashboard`)
|
|
22
|
+
|
|
23
|
+
## 3. API 规范 (API Patterns)
|
|
24
|
+
- **API客户端**:使用统一的axios实例配置,位于 `src/services/apiClient.ts`
|
|
25
|
+
- **请求模式**:所有API调用必须封装在自定义hooks (`useApi`, `useFetch`) 或服务函数中
|
|
26
|
+
- **响应结构**:
|
|
27
|
+
```typescript
|
|
28
|
+
interface ApiResponse<T> {
|
|
29
|
+
success: boolean;
|
|
30
|
+
data: T;
|
|
31
|
+
message?: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
code?: number;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
- **错误处理**:统一使用try-catch包装,错误通过状态返回而非抛出异常
|
|
37
|
+
- **认证**:Token存储在localStorage,通过axios拦截器自动添加Authorization头
|
|
38
|
+
- **路由组织**:
|
|
39
|
+
- 公开路由:无需认证 (`/login`, `/register`)
|
|
40
|
+
- 受保护路由:需要认证 (`/dashboard`, `/profile`)
|
|
41
|
+
- 管理员路由:需要特定角色 (`/admin/*`)
|
|
42
|
+
|
|
43
|
+
## 4. 数据层规范 (Data Layer Rules)
|
|
44
|
+
- **状态管理**:
|
|
45
|
+
- 全局状态:React Context + useReducer
|
|
46
|
+
- 本地组件状态:useState
|
|
47
|
+
- 服务端状态:React Query或自定义缓存hooks
|
|
48
|
+
- **数据获取**:所有数据必须通过服务层获取,禁止组件直接调用fetch/axios
|
|
49
|
+
- **类型定义**:所有API请求/响应必须定义在 `src/types/` 目录下
|
|
50
|
+
- **数据转换**:在服务层进行API响应到前端模型的转换
|
|
51
|
+
|
|
52
|
+
## 5. 错误处理规范 (Error Handling Patterns)
|
|
53
|
+
- **全局错误边界**:App组件包裹ErrorBoundary
|
|
54
|
+
- **API错误**:统一在服务层处理,返回标准化错误对象
|
|
55
|
+
- **用户反馈**:通过Toast/Snackbar组件显示错误消息
|
|
56
|
+
- **错误日志**:使用console.error记录,生产环境集成错误监控
|
|
57
|
+
- **表单验证**:使用Formik或React Hook Form进行客户端验证
|
|
58
|
+
|
|
59
|
+
## 6. 禁区 (Red Lines — Never Violate)
|
|
60
|
+
- [ ] **禁止**在组件中直接进行API调用(必须通过hooks/服务层)
|
|
61
|
+
- [ ] **禁止**使用`any`类型(必须明确定义或使用`unknown`)
|
|
62
|
+
- [ ] **禁止**修改传入的props(遵循单向数据流)
|
|
63
|
+
- [ ] **禁止**在组件中编写复杂业务逻辑(必须提取到hooks/服务层)
|
|
64
|
+
- [ ] **禁止**使用内联样式(必须使用CSS模块或styled-components)
|
|
65
|
+
- [ ] **禁止**提交未定义类型的代码
|
|
66
|
+
- [ ] **禁止**直接访问process.env(必须通过配置模块)
|
|
67
|
+
|
|
68
|
+
## 7. 测试规范 (Testing Rules)
|
|
69
|
+
- **测试框架**:Vitest + React Testing Library
|
|
70
|
+
- **测试文件位置**:与源文件同目录,使用`.test.tsx`或`.spec.tsx`后缀
|
|
71
|
+
- **必须测试**:
|
|
72
|
+
- 所有自定义hooks
|
|
73
|
+
- 复杂业务逻辑函数
|
|
74
|
+
- 关键用户交互流程
|
|
75
|
+
- 组件渲染和状态变化
|
|
76
|
+
- **测试结构**:Arrange-Act-Assert模式
|
|
77
|
+
- **Mock策略**:API调用必须mock,使用MSW或手动mock
|
|
78
|
+
- **覆盖率要求**:核心业务逻辑必须>80%
|
|
79
|
+
|
|
80
|
+
## 8. 共享配置文件清单 (Shared Config Files — Append-Only)
|
|
81
|
+
|
|
82
|
+
CRITICAL: The following files are **singleton config files** that already exist in the project.
|
|
83
|
+
When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
|
|
84
|
+
appended/merged into these existing files. **NEVER create a new parallel file.**
|
|
85
|
+
|
|
86
|
+
- `src/constants/apiEndpoints.ts` — API端点配置 — **MODIFY ONLY, never recreate**
|
|
87
|
+
- `src/types/api.d.ts` — API类型定义 — **MODIFY ONLY, never recreate**
|
|
88
|
+
- `src/config/env.ts` — 环境变量配置 — **MODIFY ONLY, never recreate**
|
|
89
|
+
- `src/utils/errorCodes.ts` — 错误码常量 — **MODIFY ONLY, never recreate**
|
|
90
|
+
- `src/services/apiClient.ts` — Axios实例配置 — **MODIFY ONLY, never recreate**
|
|
91
|
+
- `src/routes/index.tsx` — 路由配置 — **MODIFY ONLY, never recreate**
|
|
92
|
+
- `src/hooks/index.ts` — 自定义hooks导出 — **MODIFY ONLY, never recreate**
|
|
93
|
+
- `src/context/index.ts` — Context导出 — **MODIFY ONLY, never recreate**
|
|
94
|
+
|
|
95
|
+
(No i18n/locale files detected — will be populated on first run)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "demo-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "tsc && vite build",
|
|
8
|
+
"test": "vitest run"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react": "^18.3.1",
|
|
12
|
+
"react-dom": "^18.3.1",
|
|
13
|
+
"axios": "^1.7.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.7.0",
|
|
17
|
+
"@types/react": "^18.3.0",
|
|
18
|
+
"@types/react-dom": "^18.3.0",
|
|
19
|
+
"vite": "^5.4.0",
|
|
20
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
21
|
+
"vitest": "^2.1.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -147,6 +147,100 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
|
|
|
147
147
|
}
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
// core/cli-ui.ts
|
|
151
|
+
function startSpinner(text) {
|
|
152
|
+
const isTTY = process.stderr.isTTY;
|
|
153
|
+
let frame = 0;
|
|
154
|
+
let currentText = text;
|
|
155
|
+
let stopped = false;
|
|
156
|
+
function render() {
|
|
157
|
+
if (stopped) return;
|
|
158
|
+
const symbol = import_chalk.default.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
|
|
159
|
+
if (isTTY) {
|
|
160
|
+
process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
|
|
161
|
+
}
|
|
162
|
+
frame++;
|
|
163
|
+
}
|
|
164
|
+
if (!isTTY) {
|
|
165
|
+
process.stderr.write(` \u2026 ${currentText}
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
const timer = setInterval(render, 80);
|
|
169
|
+
render();
|
|
170
|
+
return {
|
|
171
|
+
update(newText) {
|
|
172
|
+
currentText = newText;
|
|
173
|
+
},
|
|
174
|
+
stop(finalText) {
|
|
175
|
+
if (stopped) return;
|
|
176
|
+
stopped = true;
|
|
177
|
+
clearInterval(timer);
|
|
178
|
+
if (isTTY) {
|
|
179
|
+
process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
|
|
180
|
+
}
|
|
181
|
+
if (finalText) {
|
|
182
|
+
process.stderr.write(` ${finalText}
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
succeed(successText) {
|
|
187
|
+
this.stop(import_chalk.default.green(`\u2714 ${successText}`));
|
|
188
|
+
},
|
|
189
|
+
fail(failText) {
|
|
190
|
+
this.stop(import_chalk.default.red(`\u2718 ${failText}`));
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function retryCountdown(opts) {
|
|
195
|
+
const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
|
|
196
|
+
const isTTY = process.stderr.isTTY;
|
|
197
|
+
const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
|
|
198
|
+
process.stderr.write("\n");
|
|
199
|
+
process.stderr.write(import_chalk.default.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + import_chalk.default.gray(`[${label}]`) + import_chalk.default.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
|
|
200
|
+
`));
|
|
201
|
+
process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.white(shortErr) + "\n");
|
|
202
|
+
process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.gray(`Waiting before retry...`) + "\n");
|
|
203
|
+
const totalSeconds = Math.ceil(waitMs / 1e3);
|
|
204
|
+
for (let s = totalSeconds; s > 0; s--) {
|
|
205
|
+
const bar = import_chalk.default.green("\u2588".repeat(totalSeconds - s)) + import_chalk.default.gray("\u2591".repeat(s));
|
|
206
|
+
const line = import_chalk.default.yellow(` \u2502 `) + `${bar} ${import_chalk.default.bold.white(`${s}s`)}`;
|
|
207
|
+
if (isTTY) {
|
|
208
|
+
process.stderr.write(`\r${line}${" ".repeat(10)}`);
|
|
209
|
+
}
|
|
210
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
211
|
+
}
|
|
212
|
+
if (isTTY) {
|
|
213
|
+
process.stderr.write(`\r${" ".repeat(70)}\r`);
|
|
214
|
+
}
|
|
215
|
+
process.stderr.write(import_chalk.default.yellow(` \u2514\u2500 `) + import_chalk.default.cyan(`Retrying now...`) + "\n\n");
|
|
216
|
+
}
|
|
217
|
+
function startStage(stageKey, label) {
|
|
218
|
+
const icon = STAGE_ICONS[stageKey] ?? "\u25B8";
|
|
219
|
+
return startSpinner(`${icon} ${label}`);
|
|
220
|
+
}
|
|
221
|
+
var import_chalk, SPINNER_FRAMES, STAGE_ICONS;
|
|
222
|
+
var init_cli_ui = __esm({
|
|
223
|
+
"core/cli-ui.ts"() {
|
|
224
|
+
"use strict";
|
|
225
|
+
import_chalk = __toESM(require("chalk"));
|
|
226
|
+
SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
227
|
+
STAGE_ICONS = {
|
|
228
|
+
context_load: "\u{1F4C2}",
|
|
229
|
+
design_dialogue: "\u{1F4AC}",
|
|
230
|
+
spec_gen: "\u{1F4DD}",
|
|
231
|
+
spec_refine: "\u270F\uFE0F ",
|
|
232
|
+
spec_assess: "\u{1F4CA}",
|
|
233
|
+
dsl_extract: "\u{1F517}",
|
|
234
|
+
dsl_gap_feedback: "\u{1F50D}",
|
|
235
|
+
codegen: "\u2699\uFE0F ",
|
|
236
|
+
test_gen: "\u{1F9EA}",
|
|
237
|
+
error_feedback: "\u{1F527}",
|
|
238
|
+
review: "\u{1F50E}",
|
|
239
|
+
self_eval: "\u{1F4C8}"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
150
244
|
// core/provider-utils.ts
|
|
151
245
|
function classifyError(err, label) {
|
|
152
246
|
const e = err;
|
|
@@ -223,21 +317,23 @@ async function withReliability(fn, opts) {
|
|
|
223
317
|
throw classifyError(err, label);
|
|
224
318
|
}
|
|
225
319
|
const waitMs = attempt === 0 ? 2e3 : 6e3;
|
|
226
|
-
console.warn(
|
|
227
|
-
import_chalk.default.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + import_chalk.default.gray(` \u2014 ${err.message}`)
|
|
228
|
-
);
|
|
229
320
|
onRetry?.(attempt + 1, err);
|
|
230
|
-
await
|
|
321
|
+
await retryCountdown({
|
|
322
|
+
attempt: attempt + 1,
|
|
323
|
+
maxAttempts: retries + 1,
|
|
324
|
+
waitMs,
|
|
325
|
+
errorMessage: err.message ?? String(err),
|
|
326
|
+
label
|
|
327
|
+
});
|
|
231
328
|
}
|
|
232
329
|
}
|
|
233
330
|
throw new Error("unreachable");
|
|
234
331
|
}
|
|
235
|
-
var
|
|
332
|
+
var ProviderError;
|
|
236
333
|
var init_provider_utils = __esm({
|
|
237
334
|
"core/provider-utils.ts"() {
|
|
238
335
|
"use strict";
|
|
239
|
-
|
|
240
|
-
sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
336
|
+
init_cli_ui();
|
|
241
337
|
ProviderError = class extends Error {
|
|
242
338
|
constructor(message, kind, originalError) {
|
|
243
339
|
super(message);
|
|
@@ -6711,6 +6807,7 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6711
6807
|
}
|
|
6712
6808
|
|
|
6713
6809
|
// core/code-generator.ts
|
|
6810
|
+
init_cli_ui();
|
|
6714
6811
|
var CodeGenerator = class {
|
|
6715
6812
|
constructor(provider, mode = "claude-code") {
|
|
6716
6813
|
this.provider = provider;
|
|
@@ -7159,6 +7256,7 @@ ${spec}
|
|
|
7159
7256
|
${constitutionSection}
|
|
7160
7257
|
=== ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
|
|
7161
7258
|
${existingContent || "Output only the complete file content."}`;
|
|
7259
|
+
const fileSpinner = startSpinner(`${prefix}Generating ${import_chalk9.default.bold(item.file)}...`);
|
|
7162
7260
|
try {
|
|
7163
7261
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
7164
7262
|
const fileContent = stripCodeFences(raw);
|
|
@@ -7166,11 +7264,11 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
7166
7264
|
await fs12.ensureDir(path11.dirname(fullPath));
|
|
7167
7265
|
await fs12.writeFile(fullPath, fileContent, "utf-8");
|
|
7168
7266
|
getActiveLogger()?.fileWritten(item.file);
|
|
7169
|
-
|
|
7267
|
+
fileSpinner.succeed(`${existingContent ? import_chalk9.default.yellow("~") : import_chalk9.default.green("+")} ${import_chalk9.default.bold(item.file)}`);
|
|
7170
7268
|
successCount++;
|
|
7171
7269
|
writtenFiles.push(item.file);
|
|
7172
7270
|
} catch (err) {
|
|
7173
|
-
|
|
7271
|
+
fileSpinner.fail(`${import_chalk9.default.bold(item.file)} \u2014 ${err.message}`);
|
|
7174
7272
|
}
|
|
7175
7273
|
}
|
|
7176
7274
|
if (!taskLabel) {
|
|
@@ -8196,6 +8294,7 @@ var import_chalk15 = __toESM(require("chalk"));
|
|
|
8196
8294
|
var import_child_process5 = require("child_process");
|
|
8197
8295
|
var fs18 = __toESM(require("fs-extra"));
|
|
8198
8296
|
var path17 = __toESM(require("path"));
|
|
8297
|
+
init_cli_ui();
|
|
8199
8298
|
var MAX_COMMAND_OUTPUT_CHARS = 5e4;
|
|
8200
8299
|
var MAX_FIX_FILE_CHARS = 6e4;
|
|
8201
8300
|
function runCommand(cmd, cwd) {
|
|
@@ -8387,16 +8486,17 @@ ${errorSummary}
|
|
|
8387
8486
|
${fileContent}
|
|
8388
8487
|
|
|
8389
8488
|
Output ONLY the complete fixed file content. No markdown fences, no explanations.`;
|
|
8489
|
+
const fixSpinner = startSpinner(`Fixing ${import_chalk15.default.bold(file)} (${fileErrors.length} error(s))...`);
|
|
8390
8490
|
try {
|
|
8391
8491
|
const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
|
|
8392
8492
|
const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
8393
8493
|
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
8394
8494
|
await fs18.writeFile(fullPath, fixed, "utf-8");
|
|
8395
8495
|
results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
|
|
8396
|
-
|
|
8496
|
+
fixSpinner.succeed(`Auto-fixed: ${file}`);
|
|
8397
8497
|
} catch (err) {
|
|
8398
8498
|
results.push({ fixed: false, file, explanation: `AI fix failed: ${err.message}` });
|
|
8399
|
-
|
|
8499
|
+
fixSpinner.fail(`Could not auto-fix: ${file}`);
|
|
8400
8500
|
}
|
|
8401
8501
|
}
|
|
8402
8502
|
return results;
|
|
@@ -11272,6 +11372,7 @@ async function listVcrRecordings(workingDir) {
|
|
|
11272
11372
|
}
|
|
11273
11373
|
|
|
11274
11374
|
// cli/pipeline/single-repo.ts
|
|
11375
|
+
init_cli_ui();
|
|
11275
11376
|
async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
11276
11377
|
const specProviderName = opts.provider || config2.provider || "gemini";
|
|
11277
11378
|
const specModelName = opts.model || config2.model || DEFAULT_MODELS[specProviderName];
|
|
@@ -11334,7 +11435,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11334
11435
|
console.log(import_chalk25.default.yellow(` \u26A0 Constitution is long (${context.constitution.length.toLocaleString()} chars). Consider running: ai-spec init --consolidate`));
|
|
11335
11436
|
}
|
|
11336
11437
|
} else {
|
|
11337
|
-
|
|
11438
|
+
const constitutionSpinner = startSpinner("Constitution not found \u2014 auto-generating...");
|
|
11338
11439
|
try {
|
|
11339
11440
|
const constitutionGen = new ConstitutionGenerator(
|
|
11340
11441
|
createProvider(specProviderName, specApiKey, specModelName)
|
|
@@ -11342,9 +11443,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11342
11443
|
const constitutionContent = await constitutionGen.generate(currentDir);
|
|
11343
11444
|
await constitutionGen.saveConstitution(currentDir, constitutionContent);
|
|
11344
11445
|
context.constitution = constitutionContent;
|
|
11345
|
-
|
|
11446
|
+
constitutionSpinner.succeed("Constitution generated and saved (.ai-spec-constitution.md)");
|
|
11346
11447
|
} catch (err) {
|
|
11347
|
-
|
|
11448
|
+
constitutionSpinner.fail(`Constitution auto-generation failed (${err.message}), continuing without it.`);
|
|
11348
11449
|
}
|
|
11349
11450
|
}
|
|
11350
11451
|
let architectureDecision;
|
|
@@ -11375,17 +11476,18 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11375
11476
|
let initialSpec;
|
|
11376
11477
|
let initialTasks = [];
|
|
11377
11478
|
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
11479
|
+
const specSpinner = startStage("spec_gen", `Generating spec with ${specProviderName}/${specModelName}...`);
|
|
11378
11480
|
try {
|
|
11379
11481
|
if (opts.skipTasks) {
|
|
11380
11482
|
const { SpecGenerator: SpecGenerator2 } = await Promise.resolve().then(() => (init_spec_generator(), spec_generator_exports));
|
|
11381
11483
|
const generator = new SpecGenerator2(specProvider);
|
|
11382
11484
|
initialSpec = await generator.generateSpec(idea, context, architectureDecision);
|
|
11383
|
-
|
|
11485
|
+
specSpinner.succeed("Spec generated.");
|
|
11384
11486
|
} else {
|
|
11385
11487
|
const result = await generateSpecWithTasks(specProvider, idea, context, architectureDecision);
|
|
11386
11488
|
initialSpec = result.spec;
|
|
11387
11489
|
initialTasks = result.tasks;
|
|
11388
|
-
|
|
11490
|
+
specSpinner.succeed("Spec generated.");
|
|
11389
11491
|
if (initialTasks.length > 0) {
|
|
11390
11492
|
console.log(import_chalk25.default.green(` \u2714 ${initialTasks.length} tasks generated (combined call).`));
|
|
11391
11493
|
} else {
|
|
@@ -11394,8 +11496,8 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11394
11496
|
}
|
|
11395
11497
|
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
11396
11498
|
} catch (err) {
|
|
11499
|
+
specSpinner.fail(`Spec generation failed: ${err.message}`);
|
|
11397
11500
|
runLogger.stageFail("spec_gen", err.message);
|
|
11398
|
-
console.error(import_chalk25.default.red(" \u2718 Spec generation failed:"), err);
|
|
11399
11501
|
process.exit(1);
|
|
11400
11502
|
}
|
|
11401
11503
|
let finalSpec;
|
|
@@ -11417,7 +11519,9 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11417
11519
|
console.log(import_chalk25.default.blue("\n[3.4/6] Spec quality assessment..."));
|
|
11418
11520
|
}
|
|
11419
11521
|
runLogger.stageStart("spec_assess");
|
|
11522
|
+
const assessSpinner = startStage("spec_assess", "Evaluating spec quality...");
|
|
11420
11523
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
11524
|
+
assessSpinner.stop();
|
|
11421
11525
|
if (assessment) {
|
|
11422
11526
|
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
11423
11527
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
@@ -11509,21 +11613,24 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11509
11613
|
console.log(import_chalk25.default.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
11510
11614
|
console.log(import_chalk25.default.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
11511
11615
|
runLogger.stageStart("dsl_extract");
|
|
11616
|
+
const dslSpinner = startStage("dsl_extract", "Extracting DSL from spec...");
|
|
11512
11617
|
try {
|
|
11513
11618
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
11514
|
-
if (isFrontend)
|
|
11619
|
+
if (isFrontend) {
|
|
11620
|
+
dslSpinner.update("\u{1F517} Extracting DSL (frontend ComponentSpec mode)...");
|
|
11621
|
+
}
|
|
11515
11622
|
const dslExtractor = new DslExtractor(specProvider);
|
|
11516
11623
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
11517
11624
|
if (extractedDsl) {
|
|
11518
11625
|
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
11519
|
-
|
|
11626
|
+
dslSpinner.succeed("DSL extracted and validated.");
|
|
11520
11627
|
} else {
|
|
11521
11628
|
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
11522
|
-
|
|
11629
|
+
dslSpinner.fail("DSL skipped \u2014 codegen will use Spec + Tasks only.");
|
|
11523
11630
|
}
|
|
11524
11631
|
} catch (err) {
|
|
11525
11632
|
runLogger.stageFail("dsl_extract", err.message);
|
|
11526
|
-
|
|
11633
|
+
dslSpinner.fail(`DSL extraction error: ${err.message} \u2014 continuing without DSL.`);
|
|
11527
11634
|
}
|
|
11528
11635
|
}
|
|
11529
11636
|
if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
|
|
@@ -11698,6 +11805,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11698
11805
|
if (!opts.skipReview) {
|
|
11699
11806
|
console.log(import_chalk25.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
11700
11807
|
runLogger.stageStart("review");
|
|
11808
|
+
const reviewSpinner = startStage("review", "Running 3-pass code review...");
|
|
11701
11809
|
const reviewer = new CodeReviewer(specProvider, workingDir);
|
|
11702
11810
|
const savedSpec = await fs25.readFile(specFile, "utf-8");
|
|
11703
11811
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
@@ -11705,6 +11813,7 @@ async function runSingleRepoPipeline(idea, opts, currentDir, config2) {
|
|
|
11705
11813
|
} else {
|
|
11706
11814
|
reviewResult = await reviewer.reviewCode(savedSpec, specFile);
|
|
11707
11815
|
}
|
|
11816
|
+
reviewSpinner.succeed("Code review complete.");
|
|
11708
11817
|
runLogger.stageEnd("review");
|
|
11709
11818
|
const complianceScore = extractComplianceScore(reviewResult);
|
|
11710
11819
|
const missingCount = extractMissingCount(reviewResult);
|