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
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
3
3
|
import Anthropic from "@anthropic-ai/sdk";
|
|
4
4
|
import OpenAI from "openai";
|
|
5
|
+
import axios from "axios";
|
|
5
6
|
import { ProxyAgent } from "undici";
|
|
6
7
|
|
|
7
8
|
// prompts/spec.prompt.ts
|
|
@@ -116,12 +117,21 @@ function geminiRequestOptions() {
|
|
|
116
117
|
}
|
|
117
118
|
var PROVIDER_CATALOG = {
|
|
118
119
|
// ── International ──────────────────────────────────────────────────────────
|
|
120
|
+
mimo: {
|
|
121
|
+
displayName: "MiMo (Xiaomi)",
|
|
122
|
+
description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
|
|
123
|
+
models: ["mimo-v2-pro"],
|
|
124
|
+
envKey: "MIMO_API_KEY"
|
|
125
|
+
// baseURL not used — MiMo has a dedicated provider class
|
|
126
|
+
},
|
|
119
127
|
gemini: {
|
|
120
128
|
displayName: "Google Gemini",
|
|
121
|
-
description: "Google AI Studio \u2014 Gemini 2.5 /
|
|
129
|
+
description: "Google AI Studio \u2014 Gemini 2.5 / 2.0 series",
|
|
122
130
|
models: [
|
|
123
|
-
"gemini-2.5-flash",
|
|
124
131
|
"gemini-2.5-pro",
|
|
132
|
+
"gemini-2.5-flash",
|
|
133
|
+
"gemini-2.0-flash",
|
|
134
|
+
"gemini-2.0-flash-lite",
|
|
125
135
|
"gemini-1.5-pro",
|
|
126
136
|
"gemini-1.5-flash"
|
|
127
137
|
],
|
|
@@ -129,61 +139,103 @@ var PROVIDER_CATALOG = {
|
|
|
129
139
|
},
|
|
130
140
|
claude: {
|
|
131
141
|
displayName: "Anthropic Claude",
|
|
132
|
-
description: "Anthropic \u2014 Claude 4.x series",
|
|
142
|
+
description: "Anthropic \u2014 Claude 4.x / 3.7 series",
|
|
133
143
|
models: [
|
|
134
|
-
"claude-sonnet-4-6",
|
|
135
144
|
"claude-opus-4-6",
|
|
136
|
-
"claude-
|
|
145
|
+
"claude-sonnet-4-6",
|
|
146
|
+
"claude-haiku-4-5",
|
|
147
|
+
"claude-3-7-sonnet-20250219"
|
|
137
148
|
],
|
|
138
149
|
envKey: "ANTHROPIC_API_KEY"
|
|
139
150
|
},
|
|
140
151
|
openai: {
|
|
141
152
|
displayName: "OpenAI",
|
|
142
|
-
description: "OpenAI \u2014 GPT-4o
|
|
143
|
-
models: [
|
|
153
|
+
description: "OpenAI \u2014 o3 / GPT-4o series",
|
|
154
|
+
models: [
|
|
155
|
+
"o3",
|
|
156
|
+
"o3-mini",
|
|
157
|
+
"o1",
|
|
158
|
+
"o1-mini",
|
|
159
|
+
"gpt-4o",
|
|
160
|
+
"gpt-4o-mini"
|
|
161
|
+
],
|
|
144
162
|
envKey: "OPENAI_API_KEY",
|
|
145
163
|
baseURL: "https://api.openai.com/v1"
|
|
146
164
|
},
|
|
165
|
+
deepseek: {
|
|
166
|
+
displayName: "DeepSeek",
|
|
167
|
+
description: "DeepSeek \u2014 V3 (chat) / R1 (reasoning)",
|
|
168
|
+
models: [
|
|
169
|
+
"deepseek-chat",
|
|
170
|
+
// DeepSeek-V3
|
|
171
|
+
"deepseek-reasoner"
|
|
172
|
+
// DeepSeek-R1
|
|
173
|
+
],
|
|
174
|
+
envKey: "DEEPSEEK_API_KEY",
|
|
175
|
+
baseURL: "https://api.deepseek.com/v1"
|
|
176
|
+
},
|
|
147
177
|
// ── Chinese Models (OpenAI-compatible) ────────────────────────────────────
|
|
148
178
|
qwen: {
|
|
149
179
|
displayName: "\u901A\u4E49\u5343\u95EE (Qwen)",
|
|
150
|
-
description: "\u963F\u91CC\u4E91\u767E\u70BC
|
|
180
|
+
description: "\u963F\u91CC\u4E91\u767E\u70BC \u2014 Qwen3 / Qwen2.5 series",
|
|
151
181
|
models: [
|
|
182
|
+
"qwen3-235b-a22b",
|
|
183
|
+
// Qwen3 MoE flagship (supports thinking mode)
|
|
184
|
+
"qwen3-72b",
|
|
185
|
+
"qwen3-32b",
|
|
186
|
+
"qwen3-8b",
|
|
152
187
|
"qwen-max",
|
|
153
188
|
"qwen-max-latest",
|
|
154
189
|
"qwen-plus",
|
|
155
|
-
"qwen-plus-latest",
|
|
156
|
-
"qwen-turbo",
|
|
157
190
|
"qwen-long"
|
|
158
191
|
],
|
|
159
192
|
envKey: "DASHSCOPE_API_KEY",
|
|
160
|
-
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
193
|
+
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
194
|
+
// Qwen3 models enable thinking (CoT) by default, which pollutes structured outputs.
|
|
195
|
+
// Disable it so JSON/Markdown responses stay clean.
|
|
196
|
+
extraBody: { enable_thinking: false }
|
|
197
|
+
},
|
|
198
|
+
glm: {
|
|
199
|
+
displayName: "\u667A\u8C31 GLM (Zhipu AI)",
|
|
200
|
+
description: "\u667A\u8C31 AI \u2014 GLM-5 / GLM-4 series + Z1 reasoning",
|
|
201
|
+
models: [
|
|
202
|
+
"glm-5",
|
|
203
|
+
// GLM-5 flagship (如不可用请确认最新 model ID)
|
|
204
|
+
"glm-5-flash",
|
|
205
|
+
"glm-z1",
|
|
206
|
+
// GLM-Z1 reasoning model
|
|
207
|
+
"glm-z1-flash",
|
|
208
|
+
"glm-4-plus",
|
|
209
|
+
"glm-4-flash",
|
|
210
|
+
"glm-4-long"
|
|
211
|
+
],
|
|
212
|
+
envKey: "ZHIPU_API_KEY",
|
|
213
|
+
baseURL: "https://open.bigmodel.cn/api/paas/v4/"
|
|
161
214
|
},
|
|
162
215
|
minimax: {
|
|
163
216
|
displayName: "MiniMax",
|
|
164
|
-
description: "MiniMax AI \u2014 MiniMax-Text-
|
|
217
|
+
description: "MiniMax AI \u2014 MiniMax-Text-2.7 / Text-01 series",
|
|
165
218
|
models: [
|
|
219
|
+
"MiniMax-Text-2.7",
|
|
220
|
+
// MiniMax 最新旗舰 (如不可用请确认最新 model ID)
|
|
166
221
|
"MiniMax-Text-01",
|
|
167
|
-
"abab6.5s-chat"
|
|
168
|
-
"abab6.5g-chat",
|
|
169
|
-
"abab5.5-chat"
|
|
222
|
+
"abab6.5s-chat"
|
|
170
223
|
],
|
|
171
224
|
envKey: "MINIMAX_API_KEY",
|
|
172
225
|
baseURL: "https://api.minimax.chat/v1"
|
|
173
226
|
},
|
|
174
|
-
|
|
175
|
-
displayName: "\
|
|
176
|
-
description: "\
|
|
227
|
+
doubao: {
|
|
228
|
+
displayName: "\u8C46\u5305 Doubao (ByteDance)",
|
|
229
|
+
description: "\u706B\u5C71\u5F15\u64CE Ark \u2014 Doubao Pro/Lite series",
|
|
177
230
|
models: [
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"glm-4-long"
|
|
231
|
+
"doubao-pro-256k",
|
|
232
|
+
"doubao-pro-128k",
|
|
233
|
+
"doubao-pro-32k",
|
|
234
|
+
"doubao-lite-128k",
|
|
235
|
+
"doubao-lite-32k"
|
|
184
236
|
],
|
|
185
|
-
envKey: "
|
|
186
|
-
baseURL: "https://
|
|
237
|
+
envKey: "ARK_API_KEY",
|
|
238
|
+
baseURL: "https://ark.cn-beijing.volces.com/api/v3"
|
|
187
239
|
}
|
|
188
240
|
};
|
|
189
241
|
var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_CATALOG);
|
|
@@ -234,9 +286,13 @@ var OpenAICompatibleProvider = class {
|
|
|
234
286
|
client;
|
|
235
287
|
providerName;
|
|
236
288
|
modelName;
|
|
237
|
-
|
|
289
|
+
systemRole;
|
|
290
|
+
extraBody;
|
|
291
|
+
constructor(providerName, apiKey, modelName, baseURL, systemRole = "system", extraBody) {
|
|
238
292
|
this.providerName = providerName;
|
|
239
293
|
this.modelName = modelName;
|
|
294
|
+
this.systemRole = systemRole;
|
|
295
|
+
this.extraBody = extraBody;
|
|
240
296
|
this.client = new OpenAI({
|
|
241
297
|
apiKey,
|
|
242
298
|
...baseURL ? { baseURL } : {}
|
|
@@ -245,16 +301,57 @@ var OpenAICompatibleProvider = class {
|
|
|
245
301
|
async generate(prompt, systemInstruction) {
|
|
246
302
|
const messages = [];
|
|
247
303
|
if (systemInstruction) {
|
|
248
|
-
|
|
304
|
+
const isOSeries = /^o[13]/.test(this.modelName);
|
|
305
|
+
const role = isOSeries ? "developer" : this.systemRole;
|
|
306
|
+
messages.push({ role, content: systemInstruction });
|
|
249
307
|
}
|
|
250
308
|
messages.push({ role: "user", content: prompt });
|
|
251
309
|
const completion = await this.client.chat.completions.create({
|
|
252
310
|
model: this.modelName,
|
|
253
|
-
messages
|
|
311
|
+
messages,
|
|
312
|
+
...this.extraBody ? { extra_body: this.extraBody } : {}
|
|
254
313
|
});
|
|
255
314
|
return completion.choices[0].message.content ?? "";
|
|
256
315
|
}
|
|
257
316
|
};
|
|
317
|
+
var MiMoProvider = class {
|
|
318
|
+
providerName = "mimo";
|
|
319
|
+
modelName;
|
|
320
|
+
apiKey;
|
|
321
|
+
baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
|
|
322
|
+
constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
|
|
323
|
+
this.apiKey = apiKey;
|
|
324
|
+
this.modelName = modelName;
|
|
325
|
+
}
|
|
326
|
+
async generate(prompt, systemInstruction) {
|
|
327
|
+
const body = {
|
|
328
|
+
model: this.modelName,
|
|
329
|
+
max_tokens: 16384,
|
|
330
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
331
|
+
top_p: 0.95,
|
|
332
|
+
stream: false,
|
|
333
|
+
temperature: 1,
|
|
334
|
+
stop_sequences: null
|
|
335
|
+
};
|
|
336
|
+
if (systemInstruction) {
|
|
337
|
+
body.system = systemInstruction;
|
|
338
|
+
}
|
|
339
|
+
const response = await axios.post(this.baseUrl, body, {
|
|
340
|
+
headers: {
|
|
341
|
+
"api-key": this.apiKey,
|
|
342
|
+
"Content-Type": "application/json"
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
const data = response.data;
|
|
346
|
+
const blocks = data?.content ?? [];
|
|
347
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
348
|
+
if (textBlock?.text) return textBlock.text;
|
|
349
|
+
if (data?.stop_reason === "max_tokens") {
|
|
350
|
+
throw new Error(`MiMo response truncated (max_tokens reached). The prompt may be too long. Try a shorter spec or switch to a model with larger context.`);
|
|
351
|
+
}
|
|
352
|
+
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
258
355
|
function createProvider(providerName, apiKey, modelName) {
|
|
259
356
|
const meta = PROVIDER_CATALOG[providerName];
|
|
260
357
|
if (!meta) {
|
|
@@ -268,9 +365,18 @@ function createProvider(providerName, apiKey, modelName) {
|
|
|
268
365
|
return new GeminiProvider(apiKey, model);
|
|
269
366
|
case "claude":
|
|
270
367
|
return new ClaudeProvider(apiKey, model);
|
|
271
|
-
|
|
368
|
+
case "mimo":
|
|
369
|
+
return new MiMoProvider(apiKey, model);
|
|
370
|
+
// All OpenAI-compatible providers: openai, deepseek, qwen, glm, minimax, doubao
|
|
272
371
|
default:
|
|
273
|
-
return new OpenAICompatibleProvider(
|
|
372
|
+
return new OpenAICompatibleProvider(
|
|
373
|
+
providerName,
|
|
374
|
+
apiKey,
|
|
375
|
+
model,
|
|
376
|
+
meta.baseURL,
|
|
377
|
+
meta.systemRole ?? "system",
|
|
378
|
+
meta.extraBody
|
|
379
|
+
);
|
|
274
380
|
}
|
|
275
381
|
}
|
|
276
382
|
var SpecGenerator = class {
|
|
@@ -320,8 +426,8 @@ ${context.schema.slice(0, 3e3)}`);
|
|
|
320
426
|
};
|
|
321
427
|
|
|
322
428
|
// core/context-loader.ts
|
|
323
|
-
import * as
|
|
324
|
-
import * as
|
|
429
|
+
import * as fs3 from "fs-extra";
|
|
430
|
+
import * as path2 from "path";
|
|
325
431
|
|
|
326
432
|
// node_modules/glob/dist/esm/index.min.js
|
|
327
433
|
import { fileURLToPath as Wi } from "url";
|
|
@@ -3307,7 +3413,48 @@ var Ui = Object.assign(ts, { stream: Bt, iterate: Ut });
|
|
|
3307
3413
|
var Ze = Object.assign(Je, { glob: Je, globSync: ts, sync: Ui, globStream: Qe, stream: Ii, globStreamSync: Bt, streamSync: ji, globIterate: es, iterate: Bi, globIterateSync: Ut, iterateSync: zi, Glob: I, hasMagic: le, escape: tt, unescape: W });
|
|
3308
3414
|
Ze.glob = Ze;
|
|
3309
3415
|
|
|
3416
|
+
// core/global-constitution.ts
|
|
3417
|
+
import * as fs2 from "fs-extra";
|
|
3418
|
+
import * as path from "path";
|
|
3419
|
+
import * as os2 from "os";
|
|
3420
|
+
var GLOBAL_CONSTITUTION_FILE = ".ai-spec-global-constitution.md";
|
|
3421
|
+
var SEARCH_ROOTS = [
|
|
3422
|
+
// Workspace root is injected at runtime — see loadGlobalConstitution()
|
|
3423
|
+
os2.homedir()
|
|
3424
|
+
];
|
|
3425
|
+
async function loadGlobalConstitution(extraRoots = []) {
|
|
3426
|
+
const roots = [...extraRoots, ...SEARCH_ROOTS];
|
|
3427
|
+
for (const root of roots) {
|
|
3428
|
+
const filePath = path.join(root, GLOBAL_CONSTITUTION_FILE);
|
|
3429
|
+
if (await fs2.pathExists(filePath)) {
|
|
3430
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
3431
|
+
return { content, source: filePath };
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
return null;
|
|
3435
|
+
}
|
|
3436
|
+
function mergeConstitutions(globalContent, projectContent) {
|
|
3437
|
+
const parts = [
|
|
3438
|
+
"<!-- BEGIN GLOBAL CONSTITUTION (team baseline \u2014 lower priority) -->",
|
|
3439
|
+
globalContent.trim(),
|
|
3440
|
+
"<!-- END GLOBAL CONSTITUTION -->"
|
|
3441
|
+
];
|
|
3442
|
+
if (projectContent && projectContent.trim()) {
|
|
3443
|
+
parts.push(
|
|
3444
|
+
"",
|
|
3445
|
+
"<!-- BEGIN PROJECT CONSTITUTION (project-specific \u2014 HIGHER priority, overrides global) -->",
|
|
3446
|
+
projectContent.trim(),
|
|
3447
|
+
"<!-- END PROJECT CONSTITUTION -->"
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
return parts.join("\n");
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3310
3453
|
// core/context-loader.ts
|
|
3454
|
+
var FRONTEND_FRAMEWORKS = ["react", "vue", "next", "nuxt", "react-native", "expo", "svelte", "solid-js", "qwik"];
|
|
3455
|
+
function isFrontendDeps(deps) {
|
|
3456
|
+
return deps.some((d) => FRONTEND_FRAMEWORKS.includes(d));
|
|
3457
|
+
}
|
|
3311
3458
|
var STACK_MAP = {
|
|
3312
3459
|
react: "React",
|
|
3313
3460
|
vue: "Vue",
|
|
@@ -3341,21 +3488,150 @@ var ContextLoader = class {
|
|
|
3341
3488
|
apiStructure: []
|
|
3342
3489
|
};
|
|
3343
3490
|
try {
|
|
3344
|
-
await this.
|
|
3345
|
-
await this.
|
|
3491
|
+
const isPhp = await fs3.pathExists(path2.join(this.projectRoot, "composer.json"));
|
|
3492
|
+
const isJava = await fs3.pathExists(path2.join(this.projectRoot, "pom.xml")) || await fs3.pathExists(path2.join(this.projectRoot, "build.gradle")) || await fs3.pathExists(path2.join(this.projectRoot, "build.gradle.kts"));
|
|
3493
|
+
if (isPhp) {
|
|
3494
|
+
await this.loadComposerJson(context);
|
|
3495
|
+
await this.loadPhpRoutes(context);
|
|
3496
|
+
} else if (isJava) {
|
|
3497
|
+
await this.loadMavenOrGradle(context);
|
|
3498
|
+
await this.loadJavaApiStructure(context);
|
|
3499
|
+
} else {
|
|
3500
|
+
await this.loadPackageJson(context);
|
|
3501
|
+
await this.loadPrismaSchema(context);
|
|
3502
|
+
}
|
|
3346
3503
|
await this.loadFileStructure(context);
|
|
3347
3504
|
await this.loadApiStructure(context);
|
|
3348
3505
|
await this.loadConstitution(context);
|
|
3349
3506
|
await this.loadErrorPatterns(context);
|
|
3507
|
+
await this.loadSharedConfigFiles(context);
|
|
3350
3508
|
} catch (e) {
|
|
3351
3509
|
console.warn("Warning: Could not load full project context.", e);
|
|
3352
3510
|
}
|
|
3353
3511
|
return context;
|
|
3354
3512
|
}
|
|
3513
|
+
/** Load PHP project context from composer.json */
|
|
3514
|
+
async loadComposerJson(context) {
|
|
3515
|
+
const composerPath = path2.join(this.projectRoot, "composer.json");
|
|
3516
|
+
let composer = {};
|
|
3517
|
+
try {
|
|
3518
|
+
composer = await fs3.readJson(composerPath);
|
|
3519
|
+
} catch {
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
const require2 = composer.require ?? {};
|
|
3523
|
+
const requireDev = composer["require-dev"] ?? {};
|
|
3524
|
+
context.dependencies = [...Object.keys(require2), ...Object.keys(requireDev)];
|
|
3525
|
+
const stack = /* @__PURE__ */ new Set();
|
|
3526
|
+
stack.add("PHP");
|
|
3527
|
+
if (require2["laravel/lumen-framework"]) stack.add("Lumen");
|
|
3528
|
+
if (require2["laravel/framework"]) stack.add("Laravel");
|
|
3529
|
+
if (require2["symfony/framework-bundle"]) stack.add("Symfony");
|
|
3530
|
+
if (require2["slim/slim"]) stack.add("Slim");
|
|
3531
|
+
if (require2["illuminate/database"] || require2["laravel/lumen-framework"]) stack.add("Eloquent ORM");
|
|
3532
|
+
if (require2["doctrine/orm"]) stack.add("Doctrine ORM");
|
|
3533
|
+
if (require2["tymon/jwt-auth"]) stack.add("JWT Auth");
|
|
3534
|
+
if (require2["league/fractal"] || require2["spatie/laravel-fractal"]) stack.add("Fractal (Transformers)");
|
|
3535
|
+
const phpVersion = require2["php"];
|
|
3536
|
+
if (phpVersion) stack.add(`PHP ${phpVersion}`);
|
|
3537
|
+
context.techStack = Array.from(stack);
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Load PHP route files (routes/api.php, routes/web.php) as routeSummary.
|
|
3541
|
+
* Lumen uses these files to register API endpoints.
|
|
3542
|
+
*/
|
|
3543
|
+
async loadPhpRoutes(context) {
|
|
3544
|
+
const routeFiles = ["routes/api.php", "routes/web.php"];
|
|
3545
|
+
const parts = [];
|
|
3546
|
+
for (const rel of routeFiles) {
|
|
3547
|
+
const fullPath = path2.join(this.projectRoot, rel);
|
|
3548
|
+
if (!await fs3.pathExists(fullPath)) continue;
|
|
3549
|
+
try {
|
|
3550
|
+
const content = await fs3.readFile(fullPath, "utf-8");
|
|
3551
|
+
parts.push(`// ${rel}
|
|
3552
|
+
${content.slice(0, 1500)}`);
|
|
3553
|
+
} catch {
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
if (parts.length > 0) {
|
|
3557
|
+
context.routeSummary = parts.join("\n\n");
|
|
3558
|
+
}
|
|
3559
|
+
const controllerFiles = await Ze("app/Http/Controllers/**/*.php", {
|
|
3560
|
+
cwd: this.projectRoot,
|
|
3561
|
+
ignore: ["vendor/**"]
|
|
3562
|
+
});
|
|
3563
|
+
context.apiStructure = controllerFiles.slice(0, 20);
|
|
3564
|
+
}
|
|
3565
|
+
/** Load Java project context from pom.xml or build.gradle */
|
|
3566
|
+
async loadMavenOrGradle(context) {
|
|
3567
|
+
const pomPath = path2.join(this.projectRoot, "pom.xml");
|
|
3568
|
+
const gradlePath = path2.join(this.projectRoot, "build.gradle");
|
|
3569
|
+
const gradleKtsPath = path2.join(this.projectRoot, "build.gradle.kts");
|
|
3570
|
+
const stack = /* @__PURE__ */ new Set(["Java"]);
|
|
3571
|
+
const deps = [];
|
|
3572
|
+
if (await fs3.pathExists(pomPath)) {
|
|
3573
|
+
try {
|
|
3574
|
+
const xml = await fs3.readFile(pomPath, "utf-8");
|
|
3575
|
+
const artifactIds = [...xml.matchAll(/<artifactId>([^<]+)<\/artifactId>/g)].map((m) => m[1].trim()).filter((id, i) => i > 0);
|
|
3576
|
+
deps.push(...artifactIds);
|
|
3577
|
+
const javaVerMatch = xml.match(/<maven\.compiler\.source>(\d+)<\/maven\.compiler\.source>/);
|
|
3578
|
+
if (javaVerMatch) stack.add(`Java ${javaVerMatch[1]}`);
|
|
3579
|
+
if (deps.some((d) => d.includes("spring-boot"))) stack.add("Spring Boot");
|
|
3580
|
+
if (deps.some((d) => d.includes("spring-web") || d.includes("spring-webmvc"))) stack.add("Spring MVC");
|
|
3581
|
+
if (deps.some((d) => d.includes("mybatis"))) stack.add("MyBatis");
|
|
3582
|
+
if (deps.some((d) => d.includes("hibernate") || d.includes("spring-data-jpa"))) stack.add("JPA/Hibernate");
|
|
3583
|
+
if (deps.some((d) => d.includes("dubbo"))) stack.add("Dubbo");
|
|
3584
|
+
if (deps.some((d) => d.includes("rocketmq"))) stack.add("RocketMQ");
|
|
3585
|
+
if (deps.some((d) => d.includes("kafka"))) stack.add("Kafka");
|
|
3586
|
+
if (deps.some((d) => d.includes("redis"))) stack.add("Redis");
|
|
3587
|
+
if (deps.some((d) => d.includes("lombok"))) stack.add("Lombok");
|
|
3588
|
+
if (deps.some((d) => d.includes("feign") || d.includes("openfeign"))) stack.add("OpenFeign");
|
|
3589
|
+
if (deps.some((d) => d.includes("nacos"))) stack.add("Nacos");
|
|
3590
|
+
if (deps.some((d) => d.includes("sentinel"))) stack.add("Sentinel");
|
|
3591
|
+
} catch {
|
|
3592
|
+
}
|
|
3593
|
+
} else {
|
|
3594
|
+
const gradleFile = await fs3.pathExists(gradleKtsPath) ? gradleKtsPath : gradlePath;
|
|
3595
|
+
try {
|
|
3596
|
+
const content = await fs3.readFile(gradleFile, "utf-8");
|
|
3597
|
+
const depMatches = [...content.matchAll(/['"]([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]+):[^'"]+['"]/g)];
|
|
3598
|
+
deps.push(...depMatches.map((m) => m[2]));
|
|
3599
|
+
if (deps.some((d) => d.includes("spring-boot"))) stack.add("Spring Boot");
|
|
3600
|
+
if (deps.some((d) => d.includes("mybatis"))) stack.add("MyBatis");
|
|
3601
|
+
} catch {
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
context.techStack = Array.from(stack);
|
|
3605
|
+
context.dependencies = [...new Set(deps)];
|
|
3606
|
+
}
|
|
3607
|
+
/** Scan Java controller files for API structure */
|
|
3608
|
+
async loadJavaApiStructure(context) {
|
|
3609
|
+
const controllerFiles = await Ze("**/src/main/java/**/*Controller.java", {
|
|
3610
|
+
cwd: this.projectRoot,
|
|
3611
|
+
ignore: ["**/target/**"]
|
|
3612
|
+
});
|
|
3613
|
+
context.apiStructure = controllerFiles.slice(0, 30);
|
|
3614
|
+
const propFiles = await Ze("**/src/main/resources/application.{properties,yml,yaml}", {
|
|
3615
|
+
cwd: this.projectRoot,
|
|
3616
|
+
ignore: ["**/target/**"]
|
|
3617
|
+
});
|
|
3618
|
+
if (propFiles.length > 0 && !context.routeSummary) {
|
|
3619
|
+
const parts = [];
|
|
3620
|
+
for (const f of propFiles.slice(0, 2)) {
|
|
3621
|
+
try {
|
|
3622
|
+
const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
|
|
3623
|
+
parts.push(`// ${f}
|
|
3624
|
+
${content.slice(0, 800)}`);
|
|
3625
|
+
} catch {
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
if (parts.length > 0) context.routeSummary = parts.join("\n\n");
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3355
3631
|
async loadPackageJson(context) {
|
|
3356
|
-
const pkgPath =
|
|
3357
|
-
if (!await
|
|
3358
|
-
const pkg = await
|
|
3632
|
+
const pkgPath = path2.join(this.projectRoot, "package.json");
|
|
3633
|
+
if (!await fs3.pathExists(pkgPath)) return;
|
|
3634
|
+
const pkg = await fs3.readJson(pkgPath);
|
|
3359
3635
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3360
3636
|
context.dependencies = Object.keys(allDeps);
|
|
3361
3637
|
const detectedStack = /* @__PURE__ */ new Set();
|
|
@@ -3367,9 +3643,9 @@ var ContextLoader = class {
|
|
|
3367
3643
|
context.techStack = Array.from(detectedStack);
|
|
3368
3644
|
}
|
|
3369
3645
|
async loadPrismaSchema(context) {
|
|
3370
|
-
const schemaPath =
|
|
3371
|
-
if (await
|
|
3372
|
-
context.schema = await
|
|
3646
|
+
const schemaPath = path2.join(this.projectRoot, "prisma", "schema.prisma");
|
|
3647
|
+
if (await fs3.pathExists(schemaPath)) {
|
|
3648
|
+
context.schema = await fs3.readFile(schemaPath, "utf-8");
|
|
3373
3649
|
}
|
|
3374
3650
|
}
|
|
3375
3651
|
async loadFileStructure(context) {
|
|
@@ -3377,21 +3653,30 @@ var ContextLoader = class {
|
|
|
3377
3653
|
cwd: this.projectRoot,
|
|
3378
3654
|
ignore: [
|
|
3379
3655
|
"node_modules/**",
|
|
3656
|
+
"vendor/**",
|
|
3380
3657
|
"dist/**",
|
|
3658
|
+
"build/**",
|
|
3381
3659
|
".git/**",
|
|
3382
3660
|
"coverage/**",
|
|
3383
3661
|
"*.lock",
|
|
3384
|
-
".DS_Store"
|
|
3662
|
+
".DS_Store",
|
|
3663
|
+
"**/*.min.js",
|
|
3664
|
+
"**/*.map"
|
|
3385
3665
|
],
|
|
3386
3666
|
nodir: false,
|
|
3387
|
-
maxDepth:
|
|
3667
|
+
maxDepth: 5
|
|
3388
3668
|
});
|
|
3389
|
-
context.fileStructure = files.slice(0,
|
|
3669
|
+
context.fileStructure = files.slice(0, 120);
|
|
3390
3670
|
}
|
|
3391
3671
|
async loadConstitution(context) {
|
|
3392
|
-
const
|
|
3393
|
-
|
|
3394
|
-
|
|
3672
|
+
const projectFile = path2.join(this.projectRoot, ".ai-spec-constitution.md");
|
|
3673
|
+
const projectConstitution = await fs3.pathExists(projectFile) ? await fs3.readFile(projectFile, "utf-8") : void 0;
|
|
3674
|
+
const workspaceRoot = path2.dirname(this.projectRoot);
|
|
3675
|
+
const globalResult = await loadGlobalConstitution([workspaceRoot]);
|
|
3676
|
+
if (globalResult) {
|
|
3677
|
+
context.constitution = mergeConstitutions(globalResult.content, projectConstitution);
|
|
3678
|
+
} else if (projectConstitution) {
|
|
3679
|
+
context.constitution = projectConstitution;
|
|
3395
3680
|
}
|
|
3396
3681
|
}
|
|
3397
3682
|
async loadErrorPatterns(context) {
|
|
@@ -3402,12 +3687,16 @@ var ContextLoader = class {
|
|
|
3402
3687
|
const middlewareErrors = await Ze("src/**/middleware/**/{error,notFound}.{ts,js}", {
|
|
3403
3688
|
cwd: this.projectRoot
|
|
3404
3689
|
});
|
|
3405
|
-
const
|
|
3690
|
+
const phpErrorFiles = await Ze(
|
|
3691
|
+
"app/Exceptions/{Handler,ErrorHandler}.php",
|
|
3692
|
+
{ cwd: this.projectRoot, ignore: ["vendor/**"] }
|
|
3693
|
+
);
|
|
3694
|
+
const allErrorFiles = [.../* @__PURE__ */ new Set([...errorFiles, ...middlewareErrors, ...phpErrorFiles])].slice(0, 3);
|
|
3406
3695
|
if (allErrorFiles.length === 0) return;
|
|
3407
3696
|
const parts = [];
|
|
3408
3697
|
for (const f of allErrorFiles) {
|
|
3409
3698
|
try {
|
|
3410
|
-
const content = await
|
|
3699
|
+
const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
|
|
3411
3700
|
parts.push(`// ${f}
|
|
3412
3701
|
${content.slice(0, 800)}`);
|
|
3413
3702
|
} catch {
|
|
@@ -3417,6 +3706,70 @@ ${content.slice(0, 800)}`);
|
|
|
3417
3706
|
context.errorPatterns = parts.join("\n\n");
|
|
3418
3707
|
}
|
|
3419
3708
|
}
|
|
3709
|
+
/**
|
|
3710
|
+
* Scan for "singleton" config files that should never be duplicated.
|
|
3711
|
+
* These are append-only files: i18n bundles, constants, enums, config indices.
|
|
3712
|
+
*/
|
|
3713
|
+
async loadSharedConfigFiles(context) {
|
|
3714
|
+
const patterns = [
|
|
3715
|
+
// i18n / locales
|
|
3716
|
+
{ glob: "src/locales/**/*.{json,ts,js}", category: "i18n" },
|
|
3717
|
+
{ glob: "src/i18n/**/*.{json,ts,js}", category: "i18n" },
|
|
3718
|
+
{ glob: "locales/**/*.{json,ts,js}", category: "i18n" },
|
|
3719
|
+
{ glob: "public/locales/**/*.{json,ts,js}", category: "i18n" },
|
|
3720
|
+
// constants / enums
|
|
3721
|
+
{ glob: "src/constants/**/*.{ts,js}", category: "constants" },
|
|
3722
|
+
{ glob: "src/enums/**/*.{ts,js}", category: "enums" },
|
|
3723
|
+
{ glob: "src/**/constants.{ts,js}", category: "constants" },
|
|
3724
|
+
{ glob: "src/**/enums.{ts,js}", category: "enums" },
|
|
3725
|
+
// config
|
|
3726
|
+
{ glob: "src/config/**/*.{ts,js}", category: "config" },
|
|
3727
|
+
// ── Route registration files ────────────────────────────────────────────
|
|
3728
|
+
// Node.js / Express
|
|
3729
|
+
{ glob: "src/routes/**/index.{ts,js}", category: "route-index" },
|
|
3730
|
+
{ glob: "src/routes/index.{ts,js}", category: "route-index" },
|
|
3731
|
+
// Vue Router — root index and modules pattern
|
|
3732
|
+
{ glob: "src/router/index.{ts,js}", category: "route-index" },
|
|
3733
|
+
{ glob: "src/router/routes.{ts,js}", category: "route-index" },
|
|
3734
|
+
{ glob: "src/router/modules/**/*.{ts,js}", category: "route-index" },
|
|
3735
|
+
// React Router — standalone routes file or App entry
|
|
3736
|
+
{ glob: "src/routes.{ts,tsx,js,jsx}", category: "route-index" },
|
|
3737
|
+
{ glob: "src/router.{ts,tsx,js,jsx}", category: "route-index" },
|
|
3738
|
+
// PHP (Lumen / Laravel)
|
|
3739
|
+
{ glob: "routes/api.php", category: "route-index" },
|
|
3740
|
+
{ glob: "routes/web.php", category: "route-index" },
|
|
3741
|
+
// ── Store registration files ────────────────────────────────────────────
|
|
3742
|
+
// Pinia / Vuex index
|
|
3743
|
+
{ glob: "src/stores/index.{ts,js}", category: "store-index" },
|
|
3744
|
+
{ glob: "src/store/index.{ts,js}", category: "store-index" },
|
|
3745
|
+
{ glob: "src/store/modules/index.{ts,js}", category: "store-index" },
|
|
3746
|
+
// Redux root reducer / store setup
|
|
3747
|
+
{ glob: "src/store/rootReducer.{ts,js}", category: "store-index" },
|
|
3748
|
+
{ glob: "src/store/store.{ts,js}", category: "store-index" },
|
|
3749
|
+
{ glob: "src/app/store.{ts,js}", category: "store-index" }
|
|
3750
|
+
];
|
|
3751
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3752
|
+
const results = [];
|
|
3753
|
+
for (const { glob: pattern, category } of patterns) {
|
|
3754
|
+
const files = await Ze(pattern, {
|
|
3755
|
+
cwd: this.projectRoot,
|
|
3756
|
+
ignore: ["node_modules/**", "dist/**", "**/*.test.*", "**/*.spec.*"]
|
|
3757
|
+
});
|
|
3758
|
+
for (const filePath of files) {
|
|
3759
|
+
if (seen.has(filePath)) continue;
|
|
3760
|
+
seen.add(filePath);
|
|
3761
|
+
try {
|
|
3762
|
+
const content = await fs3.readFile(path2.join(this.projectRoot, filePath), "utf-8");
|
|
3763
|
+
const preview = content.split("\n").slice(0, 120).join("\n");
|
|
3764
|
+
results.push({ path: filePath, preview, category });
|
|
3765
|
+
} catch {
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
if (results.length > 0) {
|
|
3770
|
+
context.sharedConfigFiles = results;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3420
3773
|
async loadApiStructure(context) {
|
|
3421
3774
|
const apiFiles = await Ze(
|
|
3422
3775
|
"src/**/{routes,controllers,api,router,middleware}/**/*.{ts,js}",
|
|
@@ -3432,9 +3785,9 @@ ${content.slice(0, 800)}`);
|
|
|
3432
3785
|
if (context.apiStructure.length > 0) {
|
|
3433
3786
|
const summaryParts = [];
|
|
3434
3787
|
for (const filePath of context.apiStructure.slice(0, 8)) {
|
|
3435
|
-
const fullPath =
|
|
3788
|
+
const fullPath = path2.join(this.projectRoot, filePath);
|
|
3436
3789
|
try {
|
|
3437
|
-
const content = await
|
|
3790
|
+
const content = await fs3.readFile(fullPath, "utf-8");
|
|
3438
3791
|
const preview = content.split("\n").slice(0, 60).join("\n");
|
|
3439
3792
|
summaryParts.push(`\`\`\`
|
|
3440
3793
|
// ${filePath}
|
|
@@ -3452,7 +3805,99 @@ ${preview}
|
|
|
3452
3805
|
|
|
3453
3806
|
// core/spec-refiner.ts
|
|
3454
3807
|
import { editor, confirm, select } from "@inquirer/prompts";
|
|
3808
|
+
import chalk2 from "chalk";
|
|
3809
|
+
|
|
3810
|
+
// core/spec-versioning.ts
|
|
3455
3811
|
import chalk from "chalk";
|
|
3812
|
+
import * as fs4 from "fs-extra";
|
|
3813
|
+
function computeDiff(oldText, newText) {
|
|
3814
|
+
const oldLines = oldText.split("\n");
|
|
3815
|
+
const newLines = newText.split("\n");
|
|
3816
|
+
const m = oldLines.length;
|
|
3817
|
+
const n7 = newLines.length;
|
|
3818
|
+
const MAX = 800;
|
|
3819
|
+
if (m > MAX || n7 > MAX) {
|
|
3820
|
+
return computeSimpleDiff(oldLines, newLines);
|
|
3821
|
+
}
|
|
3822
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n7 + 1).fill(0));
|
|
3823
|
+
for (let i2 = m - 1; i2 >= 0; i2--) {
|
|
3824
|
+
for (let j3 = n7 - 1; j3 >= 0; j3--) {
|
|
3825
|
+
if (oldLines[i2] === newLines[j3]) {
|
|
3826
|
+
dp[i2][j3] = dp[i2 + 1][j3 + 1] + 1;
|
|
3827
|
+
} else {
|
|
3828
|
+
dp[i2][j3] = Math.max(dp[i2 + 1][j3], dp[i2][j3 + 1]);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
const lines = [];
|
|
3833
|
+
let i = 0, j2 = 0, lineNo = 1;
|
|
3834
|
+
while (i < m || j2 < n7) {
|
|
3835
|
+
if (i < m && j2 < n7 && oldLines[i] === newLines[j2]) {
|
|
3836
|
+
lines.push({ type: "unchanged", content: oldLines[i], lineNo: lineNo++ });
|
|
3837
|
+
i++;
|
|
3838
|
+
j2++;
|
|
3839
|
+
} else if (j2 < n7 && (i >= m || dp[i + 1][j2] <= dp[i][j2 + 1])) {
|
|
3840
|
+
lines.push({ type: "added", content: newLines[j2], lineNo: lineNo++ });
|
|
3841
|
+
j2++;
|
|
3842
|
+
} else {
|
|
3843
|
+
lines.push({ type: "removed", content: oldLines[i], lineNo });
|
|
3844
|
+
i++;
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
const added = lines.filter((l) => l.type === "added").length;
|
|
3848
|
+
const removed = lines.filter((l) => l.type === "removed").length;
|
|
3849
|
+
const unchanged = lines.filter((l) => l.type === "unchanged").length;
|
|
3850
|
+
return { added, removed, unchanged, lines };
|
|
3851
|
+
}
|
|
3852
|
+
function computeSimpleDiff(oldLines, newLines) {
|
|
3853
|
+
const lines = [
|
|
3854
|
+
...oldLines.map((c, i) => ({ type: "removed", content: c, lineNo: i + 1 })),
|
|
3855
|
+
...newLines.map((c, i) => ({ type: "added", content: c, lineNo: i + 1 }))
|
|
3856
|
+
];
|
|
3857
|
+
return { added: newLines.length, removed: oldLines.length, unchanged: 0, lines };
|
|
3858
|
+
}
|
|
3859
|
+
var CONTEXT_LINES = 3;
|
|
3860
|
+
function printDiff(diff) {
|
|
3861
|
+
if (diff.added === 0 && diff.removed === 0) {
|
|
3862
|
+
console.log(chalk.gray(" (no changes)"));
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
3865
|
+
const { lines } = diff;
|
|
3866
|
+
const changedIdxs = new Set(
|
|
3867
|
+
lines.map((l, i) => l.type !== "unchanged" ? i : -1).filter((i) => i !== -1)
|
|
3868
|
+
);
|
|
3869
|
+
const toShow = /* @__PURE__ */ new Set();
|
|
3870
|
+
for (const idx of changedIdxs) {
|
|
3871
|
+
for (let k2 = Math.max(0, idx - CONTEXT_LINES); k2 <= Math.min(lines.length - 1, idx + CONTEXT_LINES); k2++) {
|
|
3872
|
+
toShow.add(k2);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
const sorted = [...toShow].sort((a, b) => a - b);
|
|
3876
|
+
let prevIdx = -2;
|
|
3877
|
+
for (const idx of sorted) {
|
|
3878
|
+
if (idx > prevIdx + 1 && prevIdx !== -2) {
|
|
3879
|
+
console.log(chalk.cyan(" @@"));
|
|
3880
|
+
}
|
|
3881
|
+
const l = lines[idx];
|
|
3882
|
+
if (l.type === "added") {
|
|
3883
|
+
console.log(chalk.green(` + ${l.content}`));
|
|
3884
|
+
} else if (l.type === "removed") {
|
|
3885
|
+
console.log(chalk.red(` - ${l.content}`));
|
|
3886
|
+
} else {
|
|
3887
|
+
console.log(chalk.gray(` ${l.content}`));
|
|
3888
|
+
}
|
|
3889
|
+
prevIdx = idx;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
function printDiffSummary(diff, label) {
|
|
3893
|
+
const parts = [];
|
|
3894
|
+
if (diff.added > 0) parts.push(chalk.green(`+${diff.added}`));
|
|
3895
|
+
if (diff.removed > 0) parts.push(chalk.red(`-${diff.removed}`));
|
|
3896
|
+
if (parts.length === 0) parts.push(chalk.gray("no change"));
|
|
3897
|
+
console.log(chalk.bold(` ${label}: `) + parts.join(" ") + chalk.gray(` lines`));
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// core/spec-refiner.ts
|
|
3456
3901
|
var SpecRefiner = class {
|
|
3457
3902
|
constructor(provider) {
|
|
3458
3903
|
this.provider = provider;
|
|
@@ -3461,16 +3906,16 @@ var SpecRefiner = class {
|
|
|
3461
3906
|
let currentSpec = initialSpec;
|
|
3462
3907
|
let round = 1;
|
|
3463
3908
|
while (true) {
|
|
3464
|
-
console.log(
|
|
3909
|
+
console.log(chalk2.cyan(`
|
|
3465
3910
|
\u2500\u2500\u2500 Spec Review (Round ${round}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
3466
|
-
console.log(
|
|
3911
|
+
console.log(chalk2.gray(" Opening spec in editor. Save and close to continue."));
|
|
3467
3912
|
currentSpec = await editor({
|
|
3468
3913
|
message: "Review and edit the spec:",
|
|
3469
3914
|
default: currentSpec,
|
|
3470
3915
|
postfix: ".md",
|
|
3471
3916
|
waitForUserInput: false
|
|
3472
3917
|
});
|
|
3473
|
-
console.log(
|
|
3918
|
+
console.log(chalk2.green(" \u2714 Spec saved."));
|
|
3474
3919
|
const action = await select({
|
|
3475
3920
|
message: "What would you like to do?",
|
|
3476
3921
|
choices: [
|
|
@@ -3483,7 +3928,7 @@ var SpecRefiner = class {
|
|
|
3483
3928
|
break;
|
|
3484
3929
|
}
|
|
3485
3930
|
if (action === "ai") {
|
|
3486
|
-
console.log(
|
|
3931
|
+
console.log(chalk2.blue(` AI (${this.provider.providerName}/${this.provider.modelName}) is polishing the spec...`));
|
|
3487
3932
|
try {
|
|
3488
3933
|
const improved = await this.provider.generate(
|
|
3489
3934
|
`Review the following feature spec and improve it for clarity, completeness, and technical feasibility.
|
|
@@ -3493,25 +3938,30 @@ Output ONLY the improved markdown spec, nothing else.
|
|
|
3493
3938
|
${currentSpec}`,
|
|
3494
3939
|
"You are a Senior Tech Lead doing a spec review. Output only the improved Markdown."
|
|
3495
3940
|
);
|
|
3496
|
-
console.log(
|
|
3941
|
+
console.log(chalk2.yellow("\n AI has suggested improvements. Opening diff in editor..."));
|
|
3497
3942
|
const acceptImproved = await confirm({
|
|
3498
3943
|
message: "Accept AI improvements? (opens editor so you can review first)",
|
|
3499
3944
|
default: true
|
|
3500
3945
|
});
|
|
3501
3946
|
if (acceptImproved) {
|
|
3947
|
+
const diff = computeDiff(currentSpec, improved);
|
|
3948
|
+
console.log(chalk2.cyan("\n \u2500\u2500 AI Changes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3949
|
+
printDiffSummary(diff, "AI edits");
|
|
3950
|
+
printDiff(diff);
|
|
3951
|
+
console.log(chalk2.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
3502
3952
|
currentSpec = await editor({
|
|
3503
3953
|
message: "Review AI-improved spec (edit if needed, then save):",
|
|
3504
3954
|
default: improved,
|
|
3505
3955
|
postfix: ".md",
|
|
3506
3956
|
waitForUserInput: false
|
|
3507
3957
|
});
|
|
3508
|
-
console.log(
|
|
3958
|
+
console.log(chalk2.green(" \u2714 AI-improved spec accepted."));
|
|
3509
3959
|
} else {
|
|
3510
|
-
console.log(
|
|
3960
|
+
console.log(chalk2.gray(" AI improvements discarded. Keeping your version."));
|
|
3511
3961
|
}
|
|
3512
3962
|
} catch (err) {
|
|
3513
|
-
console.error(
|
|
3514
|
-
console.log(
|
|
3963
|
+
console.error(chalk2.red(" AI improvement failed:"), err);
|
|
3964
|
+
console.log(chalk2.gray(" Continuing with current spec."));
|
|
3515
3965
|
}
|
|
3516
3966
|
}
|
|
3517
3967
|
round++;
|
|
@@ -3521,10 +3971,10 @@ ${currentSpec}`,
|
|
|
3521
3971
|
};
|
|
3522
3972
|
|
|
3523
3973
|
// core/code-generator.ts
|
|
3524
|
-
import
|
|
3974
|
+
import chalk6 from "chalk";
|
|
3525
3975
|
import { execSync } from "child_process";
|
|
3526
|
-
import * as
|
|
3527
|
-
import * as
|
|
3976
|
+
import * as path6 from "path";
|
|
3977
|
+
import * as fs8 from "fs-extra";
|
|
3528
3978
|
|
|
3529
3979
|
// prompts/codegen.prompt.ts
|
|
3530
3980
|
var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
|
|
@@ -3535,30 +3985,217 @@ Rules:
|
|
|
3535
3985
|
3. Include proper error handling, input validation, and logging
|
|
3536
3986
|
4. Output ONLY raw code content \u2014 NO markdown fences, NO explanations, NO comments outside the code
|
|
3537
3987
|
5. Match the imports, exports, and module patterns visible in the existing codebase
|
|
3538
|
-
6.
|
|
3539
|
-
|
|
3540
|
-
|
|
3988
|
+
6. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content with only the new additions merged in
|
|
3989
|
+
|
|
3990
|
+
CRITICAL \u2014 Dependency Hallucination Prevention (MUST follow):
|
|
3991
|
+
7. You will be given an "=== Installed Packages ===" section listing ALL packages available in this project.
|
|
3992
|
+
NEVER import or use ANY package, library, or module that does not appear in that list.
|
|
3993
|
+
This includes (but is not limited to): i18n libraries, UI component libraries, utility libraries, state management libraries.
|
|
3994
|
+
If a feature would normally need a missing library, implement the equivalent functionality using only what IS installed.
|
|
3995
|
+
Violating this rule will break the project and is unacceptable.
|
|
3996
|
+
|
|
3997
|
+
CRITICAL \u2014 File Reuse Rules:
|
|
3998
|
+
8. NEVER create a new file if an existing file serves the same purpose. Check the shared config file list before planning any new file.
|
|
3999
|
+
9. i18n / locale files: ONLY add translation keys if an i18n file already exists in the project. If no i18n/locale files are listed in "Installed Packages" or shared config files, do NOT add any i18n code.
|
|
4000
|
+
10. constants / enums files: ALWAYS add new values to the EXISTING constants or enums file. Never create a new parallel file.
|
|
4001
|
+
11. When in doubt: prefer "modify existing" over "create new".
|
|
4002
|
+
|
|
4003
|
+
CRITICAL \u2014 Component Reuse (MUST follow):
|
|
4004
|
+
12. Before writing any UI component or element: check the "Existing reusable components" list in the context.
|
|
4005
|
+
If a component serving the same purpose already exists in src/components/, import and use it \u2014 do NOT create a duplicate.
|
|
4006
|
+
13. Check the "Existing page examples" for how the project's UI library components (e.g. antd, element-plus, arco-design) are actually used.
|
|
4007
|
+
Copy those exact component names and import patterns. Do NOT use generic HTML elements where the UI library already provides a component.
|
|
4008
|
+
|
|
4009
|
+
CRITICAL \u2014 Frontend Architecture Layer Separation (MUST follow):
|
|
4010
|
+
14. State management stores (Pinia, Vuex, Redux, Zustand) MUST NOT make HTTP requests directly.
|
|
4011
|
+
Stores call functions from the API layer (src/api/ or src/apis/). The API layer makes HTTP requests.
|
|
4012
|
+
If the existing store patterns in the context show no HTTP calls, do not add any.
|
|
4013
|
+
13. API files import the HTTP client using ONLY the exact import line shown in "HTTP client import" in the context.
|
|
4014
|
+
NEVER invent a different import path (e.g., '@/utils/request', '@/utils/http') unless that exact path appears in the provided context.
|
|
4015
|
+
|
|
4016
|
+
CRITICAL \u2014 Learn conventions from examples, do not invent them:
|
|
4017
|
+
15. The "=== Existing Shared Config Files ===" section below shows real files from the project.
|
|
4018
|
+
Study them carefully and match their exact structure, naming conventions, and patterns.
|
|
4019
|
+
- Router files: replicate the exact same file structure, path naming, and registration approach you see.
|
|
4020
|
+
- Store files: replicate the exact module pattern shown.
|
|
4021
|
+
- Do NOT apply generic framework defaults (e.g., Vue Router docs examples) if the project shows a different convention.
|
|
4022
|
+
- If you see a modules/ directory pattern in the examples, follow it. If you see a flat file pattern, follow that instead.
|
|
4023
|
+
The examples are ground truth. Your prior knowledge about "typical" project layouts is secondary.
|
|
4024
|
+
|
|
4025
|
+
CRITICAL \u2014 Route/Store index registration (MUST follow):
|
|
4026
|
+
16. When creating a new route module file (e.g., src/router/routes/taskManagement.ts), you MUST ALSO update
|
|
4027
|
+
the corresponding index file (src/router/routes/index.ts) to import the new module and add it to the export array.
|
|
4028
|
+
This is non-negotiable. A route module that is not registered in the index will never be loaded.
|
|
4029
|
+
Pattern: add "import taskManagement from './taskManagement'" at the top and "taskManagement" inside the "export default [...]".
|
|
4030
|
+
|
|
4031
|
+
CRITICAL \u2014 Cross-file function name consistency (MUST follow):
|
|
4032
|
+
17. When you see an "=== Files Already Generated in This Run ===" section, those file contents are the AUTHORITATIVE source
|
|
4033
|
+
of truth for exported function/variable names.
|
|
4034
|
+
NEVER rename, guess, or substitute alternative names. If the file exports "getTaskList", import "getTaskList" \u2014 not "getTasks".
|
|
4035
|
+
If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.`;
|
|
4036
|
+
var codeGenGoSystemPrompt = `You are a Senior Go Developer implementing features based on provided specifications.
|
|
4037
|
+
|
|
4038
|
+
Rules:
|
|
4039
|
+
1. Follow standard Go project layout (cmd/, internal/, pkg/, api/). Match whatever layout already exists in the project.
|
|
4040
|
+
2. Write idiomatic Go \u2014 use named return errors, defer for cleanup, context propagation, structured logging (slog or zap if present).
|
|
4041
|
+
3. Write complete, production-ready code \u2014 no placeholders, no TODOs, no stub implementations.
|
|
4042
|
+
4. Output ONLY raw Go code \u2014 NO markdown fences, NO explanations.
|
|
4043
|
+
5. Use Go modules (go.mod already exists). Never add a dependency without checking go.mod first.
|
|
4044
|
+
6. Error handling: always return errors up the call stack. Never ignore errors with _.
|
|
4045
|
+
7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
|
|
4046
|
+
8. HTTP handlers: match the existing router pattern (net/http ServeMux, gorilla/mux, chi, gin, echo \u2014 use whatever is in go.mod).
|
|
4047
|
+
9. Tests: use the standard testing package + testify/assert if already in go.mod.
|
|
4048
|
+
|
|
4049
|
+
CRITICAL \u2014 File Reuse Rules:
|
|
4050
|
+
10. NEVER create a parallel package if an existing one serves the purpose.
|
|
4051
|
+
11. Register routes/handlers in the EXISTING router setup file.
|
|
4052
|
+
12. Add new model structs to the EXISTING models file if one exists.`;
|
|
4053
|
+
var codeGenPythonSystemPrompt = `You are a Senior Python Developer implementing features based on provided specifications.
|
|
4054
|
+
|
|
4055
|
+
Rules:
|
|
4056
|
+
1. Follow PEP 8 and PEP 20. Match the code style visible in the existing codebase.
|
|
4057
|
+
2. Detect and match the existing framework: FastAPI, Flask, Django, or plain scripts.
|
|
4058
|
+
3. Write complete, production-ready code \u2014 no placeholders, no TODOs, no stub implementations.
|
|
4059
|
+
4. Output ONLY raw Python code \u2014 NO markdown fences, NO explanations.
|
|
4060
|
+
5. Use type annotations (Python 3.10+ style). Use Pydantic models if FastAPI is detected.
|
|
4061
|
+
6. Error handling: raise HTTPException (FastAPI/Flask) or domain exceptions \u2014 never swallow errors.
|
|
4062
|
+
7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
|
|
4063
|
+
8. Dependency management: only use packages already in requirements.txt / pyproject.toml.
|
|
4064
|
+
9. FastAPI: use APIRouter for new endpoints and include it in the main app router.
|
|
4065
|
+
10. Django: follow MVT pattern, register URLs in urls.py.
|
|
4066
|
+
|
|
4067
|
+
CRITICAL \u2014 File Reuse Rules:
|
|
4068
|
+
11. NEVER create a parallel module if an existing one serves the purpose.
|
|
4069
|
+
12. Register new routes/views in the EXISTING urls.py / router.py.
|
|
4070
|
+
13. Add new models to the EXISTING models.py if it exists \u2014 do not create a parallel models file.`;
|
|
4071
|
+
var codeGenJavaSystemPrompt = `You are a Senior Java Developer implementing features based on provided specifications.
|
|
4072
|
+
|
|
4073
|
+
Rules:
|
|
4074
|
+
1. Detect and match the existing framework: Spring Boot, Micronaut, or Quarkus.
|
|
4075
|
+
2. Follow standard layered architecture: Controller \u2192 Service \u2192 Repository. Match existing package names.
|
|
4076
|
+
3. Write complete, production-ready code \u2014 no placeholders, no TODOs.
|
|
4077
|
+
4. Output ONLY raw Java code \u2014 NO markdown fences, NO explanations.
|
|
4078
|
+
5. Use constructor injection (@Autowired on constructor, not field injection).
|
|
4079
|
+
6. Exception handling: use @ControllerAdvice / @ExceptionHandler if already present. Never swallow exceptions.
|
|
4080
|
+
7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
|
|
4081
|
+
8. Use Lombok if already in pom.xml / build.gradle (@Data, @Builder, etc.).
|
|
4082
|
+
|
|
4083
|
+
CRITICAL \u2014 File Reuse Rules:
|
|
4084
|
+
9. Register new endpoints in the EXISTING Controller class if one covers the same resource.
|
|
4085
|
+
10. Add new repository methods to the EXISTING Repository interface \u2014 never create a parallel one.`;
|
|
4086
|
+
var codeGenRustSystemPrompt = `You are a Senior Rust Developer implementing features based on provided specifications.
|
|
4087
|
+
|
|
4088
|
+
Rules:
|
|
4089
|
+
1. Detect and match the existing web framework: Axum, Actix-web, Warp, or Rocket.
|
|
4090
|
+
2. Write idiomatic Rust \u2014 use Result<T,E> everywhere, no unwrap() in production paths, use ? operator.
|
|
4091
|
+
3. Write complete, production-ready code \u2014 no placeholders, no TODOs.
|
|
4092
|
+
4. Output ONLY raw Rust code \u2014 NO markdown fences, NO explanations.
|
|
4093
|
+
5. Follow existing module structure (mod declarations in lib.rs / main.rs).
|
|
4094
|
+
6. Use existing crates from Cargo.toml only \u2014 do not add new dependencies.
|
|
4095
|
+
7. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
|
|
4096
|
+
8. Async: use tokio if already in Cargo.toml. Match existing async patterns.
|
|
4097
|
+
|
|
4098
|
+
CRITICAL \u2014 File Reuse Rules:
|
|
4099
|
+
9. Register new routes in the EXISTING router setup (match pattern in main.rs or router.rs).
|
|
4100
|
+
10. Add new model structs to the EXISTING models.rs / types.rs \u2014 never create a parallel file.`;
|
|
4101
|
+
var codeGenPhpSystemPrompt = `You are a Senior PHP Developer implementing features based on provided specifications.
|
|
4102
|
+
|
|
4103
|
+
Rules:
|
|
4104
|
+
1. Detect and match the existing framework: Lumen, Laravel, or plain PHP. Follow its directory conventions (app/Http/Controllers, app/Models, routes/web.php / routes/api.php).
|
|
4105
|
+
2. Write clean, PSR-12 compliant PHP 8.x code. Use typed properties, constructor promotion, named arguments, and match expressions where appropriate.
|
|
4106
|
+
3. Write complete, production-ready code \u2014 no placeholders, no TODOs, no stub implementations.
|
|
4107
|
+
4. Output ONLY raw PHP code \u2014 NO markdown fences, NO explanations.
|
|
4108
|
+
5. Use Eloquent ORM if already present. Never introduce raw SQL when Eloquent is available.
|
|
4109
|
+
6. Lumen: register routes in routes/web.php or routes/api.php. Laravel: use resource controllers and Route::apiResource where suitable.
|
|
4110
|
+
7. Error handling: throw proper HTTP exceptions (e.g. Illuminate\\Http\\Exceptions\\HttpResponseException) and return JSON responses consistently.
|
|
4111
|
+
8. If modifying an existing file, preserve all unchanged code exactly \u2014 return the FULL file content.
|
|
4112
|
+
9. Only use Composer packages already listed in composer.json \u2014 never add new dependencies.
|
|
4113
|
+
|
|
4114
|
+
CRITICAL \u2014 File Reuse Rules:
|
|
4115
|
+
10. Register new routes in the EXISTING routes/api.php (or routes/web.php) \u2014 never create a parallel routes file.
|
|
4116
|
+
11. Add new Eloquent model methods to the EXISTING Model class \u2014 never create a parallel model file.
|
|
4117
|
+
12. Add new service methods to the EXISTING service class if one already covers the same resource.`;
|
|
4118
|
+
function getCodeGenSystemPrompt(repoType) {
|
|
4119
|
+
switch (repoType) {
|
|
4120
|
+
case "go":
|
|
4121
|
+
return codeGenGoSystemPrompt;
|
|
4122
|
+
case "python":
|
|
4123
|
+
return codeGenPythonSystemPrompt;
|
|
4124
|
+
case "java":
|
|
4125
|
+
return codeGenJavaSystemPrompt;
|
|
4126
|
+
case "rust":
|
|
4127
|
+
return codeGenRustSystemPrompt;
|
|
4128
|
+
case "php":
|
|
4129
|
+
return codeGenPhpSystemPrompt;
|
|
4130
|
+
default:
|
|
4131
|
+
return codeGenSystemPrompt;
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
var reviewArchitectureSystemPrompt = `You are a Senior Software Architect reviewing the HIGH-LEVEL design of a code change.
|
|
4135
|
+
|
|
4136
|
+
Focus ONLY on:
|
|
4137
|
+
1. **Spec compliance** \u2014 Does the implementation match the spec? Are there missing or extra endpoints/components?
|
|
4138
|
+
2. **Layer separation** \u2014 Does each layer have the right responsibilities? (e.g., no business logic in controllers, no HTTP in stores)
|
|
4139
|
+
3. **API contract** \u2014 Are request/response shapes correct? Are all error codes from the spec implemented?
|
|
4140
|
+
4. **Data model integrity** \u2014 Are constraints, unique fields, and relationships correct?
|
|
4141
|
+
5. **Security posture** \u2014 Are auth checks applied to the right endpoints? Any obvious missing auth?
|
|
4142
|
+
|
|
4143
|
+
DO NOT comment on:
|
|
4144
|
+
- Code style, naming conventions, formatting
|
|
4145
|
+
- Minor implementation details (variable names, inline comments)
|
|
4146
|
+
- Performance micro-optimizations
|
|
3541
4147
|
|
|
3542
|
-
|
|
4148
|
+
Format:
|
|
4149
|
+
|
|
4150
|
+
## \u{1F3D7} \u67B6\u6784\u5408\u89C4\u6027 (Spec Compliance)
|
|
4151
|
+
Does the implementation match the spec? List any missing or wrong endpoints/components.
|
|
4152
|
+
|
|
4153
|
+
## \u{1F500} \u5C42\u804C\u8D23\u5206\u79BB (Layer Separation)
|
|
4154
|
+
Any layer boundary violations?
|
|
4155
|
+
|
|
4156
|
+
## \u{1F512} \u5B89\u5168\u4E0E\u6743\u9650 (Security & Auth)
|
|
4157
|
+
Any missing auth checks, exposed data, or privilege issues?
|
|
4158
|
+
|
|
4159
|
+
## \u{1F4CB} \u67B6\u6784\u8BC4\u5206 (Architecture Score)
|
|
4160
|
+
Score: X/10 \u2014 One short paragraph.
|
|
4161
|
+
|
|
4162
|
+
Be specific. Reference file names or endpoint paths.`;
|
|
4163
|
+
var reviewImplementationSystemPrompt = `You are a Senior Engineer reviewing the IMPLEMENTATION DETAILS of a code change.
|
|
4164
|
+
|
|
4165
|
+
An architecture review has already been completed (provided as context). Do NOT repeat its findings.
|
|
4166
|
+
|
|
4167
|
+
Focus ONLY on:
|
|
4168
|
+
1. **Input validation** \u2014 Are all inputs validated before use? Missing length/format/type checks?
|
|
4169
|
+
2. **Error handling** \u2014 Are all error paths handled? Any unhandled promise rejections or uncaught exceptions?
|
|
4170
|
+
3. **Edge cases** \u2014 Null/undefined handling, empty arrays, boundary conditions?
|
|
4171
|
+
4. **Code patterns** \u2014 DRY violations, overly complex logic that could be simplified, missing abstractions?
|
|
4172
|
+
5. **Past issue recurrence** \u2014 Does the code repeat any known patterns flagged in previous reviews (provided as history context)?
|
|
4173
|
+
|
|
4174
|
+
DO NOT repeat architecture-level findings already covered in the architecture review.
|
|
4175
|
+
|
|
4176
|
+
Format:
|
|
3543
4177
|
|
|
3544
4178
|
## \u2705 \u4F18\u70B9 (What's Good)
|
|
3545
|
-
|
|
4179
|
+
Specific implementation strengths.
|
|
3546
4180
|
|
|
3547
4181
|
## \u26A0\uFE0F \u95EE\u9898 (Issues Found)
|
|
3548
|
-
|
|
4182
|
+
Bugs, missing validation, error handling gaps \u2014 with file:line references where possible.
|
|
4183
|
+
|
|
4184
|
+
## \u{1F501} \u5386\u53F2\u95EE\u9898\u590D\u73B0 (Recurring Issues)
|
|
4185
|
+
Any issues that appeared in past reviews and are still present? (Only if history context is provided)
|
|
3549
4186
|
|
|
3550
4187
|
## \u{1F4A1} \u6539\u8FDB\u5EFA\u8BAE (Suggestions)
|
|
3551
|
-
Actionable improvements
|
|
4188
|
+
Actionable, concrete improvements.
|
|
3552
4189
|
|
|
3553
|
-
## \u{1F4CA} \
|
|
3554
|
-
Score: X/10 \u2014
|
|
4190
|
+
## \u{1F4CA} \u7EFC\u5408\u8BC4\u5206 (Final Score)
|
|
4191
|
+
Score: X/10 \u2014 Combined architecture + implementation assessment in one paragraph.
|
|
3555
4192
|
|
|
3556
4193
|
Be specific. Reference actual code, not vague principles.`;
|
|
3557
4194
|
|
|
3558
4195
|
// core/task-generator.ts
|
|
3559
|
-
import
|
|
3560
|
-
import * as
|
|
3561
|
-
import * as
|
|
4196
|
+
import chalk3 from "chalk";
|
|
4197
|
+
import * as fs5 from "fs-extra";
|
|
4198
|
+
import * as path3 from "path";
|
|
3562
4199
|
|
|
3563
4200
|
// prompts/tasks.prompt.ts
|
|
3564
4201
|
var tasksSystemPrompt = `You are a Senior Software Architect. Decompose the provided Feature Spec into an ordered list of discrete implementation tasks.
|
|
@@ -3571,7 +4208,7 @@ Each task object must have these exact fields:
|
|
|
3571
4208
|
"title": "...", // short action phrase, e.g. "Add UserFavorite Prisma model"
|
|
3572
4209
|
"description": "...", // 1-2 sentences, specific and actionable
|
|
3573
4210
|
"layer": "data|service|api|test|infra", // implementation layer
|
|
3574
|
-
"filesToTouch": ["..."], //
|
|
4211
|
+
"filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
|
|
3575
4212
|
"acceptanceCriteria": ["..."], // verifiable completion conditions
|
|
3576
4213
|
"dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
|
|
3577
4214
|
"priority": "high|medium|low"
|
|
@@ -3584,14 +4221,64 @@ Layer ordering guidance (implement in this order):
|
|
|
3584
4221
|
4. "api" \u2014 controllers, routes, middleware, validators
|
|
3585
4222
|
5. "test" \u2014 unit tests, integration tests
|
|
3586
4223
|
|
|
3587
|
-
Rules:
|
|
3588
|
-
-
|
|
4224
|
+
CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
|
|
4225
|
+
- ONLY use paths that appear in the "Verified File Inventory" section of the prompt.
|
|
4226
|
+
- For NEW files that don't exist yet, derive the path by following the naming pattern of sibling files already in the inventory (same directory, same extension, same casing).
|
|
4227
|
+
- For EXISTING singleton files (i18n, constants, enums, route index), you MUST use the exact path from the inventory. NEVER invent a sub-path or nested variant.
|
|
4228
|
+
- If you are unsure of the exact path for a new file, leave it as "TBD:<description>" rather than guessing.
|
|
4229
|
+
- Cross-check: after writing all tasks, verify every path in filesToTouch exists in the inventory or is a logical new sibling. If it doesn't pass this check, fix it.
|
|
4230
|
+
|
|
4231
|
+
Other rules:
|
|
3589
4232
|
- acceptanceCriteria must be verifiable (not vague like "works correctly")
|
|
3590
4233
|
- dependencies must reflect real implementation order
|
|
3591
4234
|
- Aim for 4-10 tasks total \u2014 not too granular, not too coarse
|
|
3592
4235
|
- Each task should be completable in one focused coding session`;
|
|
3593
4236
|
|
|
3594
4237
|
// core/task-generator.ts
|
|
4238
|
+
function buildVerifiedInventory(context) {
|
|
4239
|
+
const lines = ["=== Verified File Inventory (filesToTouch MUST use paths from here) ===\n"];
|
|
4240
|
+
if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
|
|
4241
|
+
lines.push("-- Shared Config Files (APPEND-ONLY \u2014 never create a parallel file) --");
|
|
4242
|
+
for (const f of context.sharedConfigFiles) {
|
|
4243
|
+
lines.push(` [${f.category}] ${f.path}`);
|
|
4244
|
+
}
|
|
4245
|
+
lines.push("");
|
|
4246
|
+
}
|
|
4247
|
+
if (context.apiStructure.length > 0) {
|
|
4248
|
+
lines.push("-- API / Route / Controller Files --");
|
|
4249
|
+
for (const f of context.apiStructure.slice(0, 20)) {
|
|
4250
|
+
lines.push(` ${f}`);
|
|
4251
|
+
}
|
|
4252
|
+
lines.push("");
|
|
4253
|
+
}
|
|
4254
|
+
if (context.fileStructure.length > 0) {
|
|
4255
|
+
lines.push("-- Project File Tree (first 60 entries) --");
|
|
4256
|
+
for (const f of context.fileStructure.slice(0, 60)) {
|
|
4257
|
+
lines.push(` ${f}`);
|
|
4258
|
+
}
|
|
4259
|
+
lines.push("");
|
|
4260
|
+
}
|
|
4261
|
+
lines.push(
|
|
4262
|
+
"REMINDER: If a needed file does not appear above and is NOT a new file, verify its path.\nFor i18n/locale files, constants, enums, or route indexes \u2014 use EXACTLY the path shown above.\n"
|
|
4263
|
+
);
|
|
4264
|
+
return lines.join("\n");
|
|
4265
|
+
}
|
|
4266
|
+
function buildTaskPrompt(spec, context) {
|
|
4267
|
+
if (!context) return spec;
|
|
4268
|
+
const parts = [spec];
|
|
4269
|
+
if (context.constitution) {
|
|
4270
|
+
parts.push(`
|
|
4271
|
+
=== Project Constitution (rules to follow) ===
|
|
4272
|
+
${context.constitution.slice(0, 1500)}`);
|
|
4273
|
+
}
|
|
4274
|
+
if (context.techStack.length > 0) {
|
|
4275
|
+
parts.push(`
|
|
4276
|
+
=== Tech Stack ===
|
|
4277
|
+
${context.techStack.join(", ")}`);
|
|
4278
|
+
}
|
|
4279
|
+
parts.push("\n" + buildVerifiedInventory(context));
|
|
4280
|
+
return parts.join("\n");
|
|
4281
|
+
}
|
|
3595
4282
|
var LAYER_ORDER = {
|
|
3596
4283
|
data: 0,
|
|
3597
4284
|
infra: 1,
|
|
@@ -3604,20 +4291,15 @@ var TaskGenerator = class {
|
|
|
3604
4291
|
this.provider = provider;
|
|
3605
4292
|
}
|
|
3606
4293
|
async generateTasks(spec, context) {
|
|
3607
|
-
const
|
|
3608
|
-
=== Project Context ===
|
|
3609
|
-
Tech: ${context.techStack.join(", ")}
|
|
3610
|
-
Files: ${context.fileStructure.slice(0, 20).join(", ")}
|
|
3611
|
-
` : "";
|
|
3612
|
-
const prompt = `${spec}${contextSummary}`;
|
|
4294
|
+
const prompt = buildTaskPrompt(spec, context);
|
|
3613
4295
|
const raw = await this.provider.generate(prompt, tasksSystemPrompt);
|
|
3614
4296
|
return parseTasks(raw);
|
|
3615
4297
|
}
|
|
3616
4298
|
async saveTasks(tasks, specFilePath) {
|
|
3617
|
-
const dir =
|
|
3618
|
-
const base =
|
|
3619
|
-
const tasksFile =
|
|
3620
|
-
await
|
|
4299
|
+
const dir = path3.dirname(specFilePath);
|
|
4300
|
+
const base = path3.basename(specFilePath, ".md");
|
|
4301
|
+
const tasksFile = path3.join(dir, `${base}-tasks.json`);
|
|
4302
|
+
await fs5.writeJson(tasksFile, tasks, { spaces: 2 });
|
|
3621
4303
|
return tasksFile;
|
|
3622
4304
|
}
|
|
3623
4305
|
sortByLayer(tasks) {
|
|
@@ -3640,103 +4322,986 @@ function parseTasks(raw) {
|
|
|
3640
4322
|
}
|
|
3641
4323
|
function printTasks(tasks) {
|
|
3642
4324
|
const layerColors = {
|
|
3643
|
-
data:
|
|
3644
|
-
infra:
|
|
3645
|
-
service:
|
|
3646
|
-
api:
|
|
3647
|
-
test:
|
|
4325
|
+
data: chalk3.magenta,
|
|
4326
|
+
infra: chalk3.gray,
|
|
4327
|
+
service: chalk3.blue,
|
|
4328
|
+
api: chalk3.cyan,
|
|
4329
|
+
test: chalk3.green
|
|
3648
4330
|
};
|
|
3649
|
-
console.log(
|
|
4331
|
+
console.log(chalk3.bold(`
|
|
3650
4332
|
Tasks (${tasks.length}):`));
|
|
3651
4333
|
for (const task of tasks) {
|
|
3652
|
-
const color = layerColors[task.layer] ??
|
|
4334
|
+
const color = layerColors[task.layer] ?? chalk3.white;
|
|
3653
4335
|
const badge = color(`[${task.layer}]`);
|
|
3654
|
-
const prio = task.priority === "high" ?
|
|
3655
|
-
console.log(` ${prio} ${
|
|
4336
|
+
const prio = task.priority === "high" ? chalk3.red("\u25CF") : task.priority === "medium" ? chalk3.yellow("\u25CF") : chalk3.gray("\u25CF");
|
|
4337
|
+
console.log(` ${prio} ${chalk3.bold(task.id)} ${badge} ${task.title}`);
|
|
3656
4338
|
}
|
|
3657
4339
|
}
|
|
3658
4340
|
async function loadTasksForSpec(specFilePath) {
|
|
3659
|
-
const base =
|
|
3660
|
-
const dir =
|
|
3661
|
-
const tasksFile =
|
|
3662
|
-
if (await
|
|
3663
|
-
return
|
|
4341
|
+
const base = path3.basename(specFilePath, ".md");
|
|
4342
|
+
const dir = path3.dirname(specFilePath);
|
|
4343
|
+
const tasksFile = path3.join(dir, `${base}-tasks.json`);
|
|
4344
|
+
if (await fs5.pathExists(tasksFile)) {
|
|
4345
|
+
return fs5.readJson(tasksFile);
|
|
3664
4346
|
}
|
|
3665
4347
|
return null;
|
|
3666
4348
|
}
|
|
4349
|
+
async function updateTaskStatus(specFilePath, taskId, status) {
|
|
4350
|
+
const tasks = await loadTasksForSpec(specFilePath);
|
|
4351
|
+
if (!tasks) return;
|
|
4352
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
4353
|
+
if (!task) return;
|
|
4354
|
+
task.status = status;
|
|
4355
|
+
const base = path3.basename(specFilePath, ".md");
|
|
4356
|
+
const dir = path3.dirname(specFilePath);
|
|
4357
|
+
await fs5.writeJson(path3.join(dir, `${base}-tasks.json`), tasks, { spaces: 2 });
|
|
4358
|
+
}
|
|
3667
4359
|
|
|
3668
|
-
// core/
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
4360
|
+
// core/dsl-extractor.ts
|
|
4361
|
+
import chalk5 from "chalk";
|
|
4362
|
+
import * as fs6 from "fs-extra";
|
|
4363
|
+
import * as path4 from "path";
|
|
4364
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
4365
|
+
|
|
4366
|
+
// core/dsl-validator.ts
|
|
4367
|
+
import chalk4 from "chalk";
|
|
4368
|
+
var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
4369
|
+
var MAX_MODELS = 50;
|
|
4370
|
+
var MAX_FIELDS_PER_MODEL = 100;
|
|
4371
|
+
var MAX_ENDPOINTS = 100;
|
|
4372
|
+
var MAX_BEHAVIORS = 50;
|
|
4373
|
+
var MAX_ERRORS_PER_ENDPOINT = 20;
|
|
4374
|
+
function validateDsl(raw) {
|
|
4375
|
+
const errors = [];
|
|
4376
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4377
|
+
return {
|
|
4378
|
+
valid: false,
|
|
4379
|
+
errors: [{ path: "root", message: "DSL must be a JSON object, got: " + typeLabel(raw) }]
|
|
4380
|
+
};
|
|
3675
4381
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
if (lines[lines.length - 1].trim() === "```") lines.pop();
|
|
3683
|
-
return lines.join("\n").trim();
|
|
3684
|
-
}
|
|
3685
|
-
function parseJsonArray(text) {
|
|
3686
|
-
const fenced = text.match(/```(?:json)?\n(\[[\s\S]*?\])\n```/);
|
|
3687
|
-
const raw = fenced ? fenced[1] : text.match(/\[[\s\S]*?\]/)?.[0] ?? "";
|
|
3688
|
-
try {
|
|
3689
|
-
const parsed = JSON.parse(raw);
|
|
3690
|
-
if (Array.isArray(parsed)) return parsed;
|
|
3691
|
-
} catch {
|
|
4382
|
+
const obj = raw;
|
|
4383
|
+
if (obj["version"] !== "1.0") {
|
|
4384
|
+
errors.push({
|
|
4385
|
+
path: "version",
|
|
4386
|
+
message: `Must be "1.0", got: ${JSON.stringify(obj["version"])}`
|
|
4387
|
+
});
|
|
3692
4388
|
}
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
4389
|
+
validateFeature(obj["feature"], "feature", errors);
|
|
4390
|
+
if (!Array.isArray(obj["models"])) {
|
|
4391
|
+
errors.push({ path: "models", message: `Must be an array, got: ${typeLabel(obj["models"])}` });
|
|
4392
|
+
} else {
|
|
4393
|
+
const models = obj["models"];
|
|
4394
|
+
if (models.length > MAX_MODELS) {
|
|
4395
|
+
errors.push({ path: "models", message: `Too many models (${models.length} > ${MAX_MODELS})` });
|
|
4396
|
+
}
|
|
4397
|
+
for (let i = 0; i < Math.min(models.length, MAX_MODELS); i++) {
|
|
4398
|
+
validateModel(models[i], `models[${i}]`, errors);
|
|
4399
|
+
}
|
|
3699
4400
|
}
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
3707
|
-
)
|
|
3708
|
-
);
|
|
3709
|
-
console.log(chalk3.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
3710
|
-
console.log(chalk3.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
3711
|
-
`));
|
|
3712
|
-
effectiveMode = "api";
|
|
4401
|
+
if (!Array.isArray(obj["endpoints"])) {
|
|
4402
|
+
errors.push({ path: "endpoints", message: `Must be an array, got: ${typeLabel(obj["endpoints"])}` });
|
|
4403
|
+
} else {
|
|
4404
|
+
const eps = obj["endpoints"];
|
|
4405
|
+
if (eps.length > MAX_ENDPOINTS) {
|
|
4406
|
+
errors.push({ path: "endpoints", message: `Too many endpoints (${eps.length} > ${MAX_ENDPOINTS})` });
|
|
3713
4407
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
return this.runClaudeCode(specFilePath, workingDir, options);
|
|
3717
|
-
case "api":
|
|
3718
|
-
return this.runApiMode(specFilePath, workingDir, context);
|
|
3719
|
-
case "plan":
|
|
3720
|
-
return this.runPlanMode(specFilePath);
|
|
4408
|
+
for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
|
|
4409
|
+
validateEndpoint(eps[i], `endpoints[${i}]`, errors);
|
|
3721
4410
|
}
|
|
3722
4411
|
}
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
4412
|
+
if (obj["behaviors"] !== void 0) {
|
|
4413
|
+
if (!Array.isArray(obj["behaviors"])) {
|
|
4414
|
+
errors.push({ path: "behaviors", message: `Must be an array if present, got: ${typeLabel(obj["behaviors"])}` });
|
|
4415
|
+
} else {
|
|
4416
|
+
const behaviors = obj["behaviors"];
|
|
4417
|
+
if (behaviors.length > MAX_BEHAVIORS) {
|
|
4418
|
+
errors.push({ path: "behaviors", message: `Too many behaviors (${behaviors.length} > ${MAX_BEHAVIORS})` });
|
|
4419
|
+
}
|
|
4420
|
+
for (let i = 0; i < Math.min(behaviors.length, MAX_BEHAVIORS); i++) {
|
|
4421
|
+
validateBehavior(behaviors[i], `behaviors[${i}]`, errors);
|
|
4422
|
+
}
|
|
3730
4423
|
}
|
|
3731
4424
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
4425
|
+
if (obj["components"] !== void 0) {
|
|
4426
|
+
if (!Array.isArray(obj["components"])) {
|
|
4427
|
+
errors.push({ path: "components", message: `Must be an array if present, got: ${typeLabel(obj["components"])}` });
|
|
4428
|
+
} else {
|
|
4429
|
+
const components = obj["components"];
|
|
4430
|
+
for (let i = 0; i < Math.min(components.length, 50); i++) {
|
|
4431
|
+
validateComponent(components[i], `components[${i}]`, errors);
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
if (errors.length > 0) {
|
|
4436
|
+
return { valid: false, errors };
|
|
4437
|
+
}
|
|
4438
|
+
return { valid: true, dsl: raw };
|
|
4439
|
+
}
|
|
4440
|
+
function validateFeature(raw, path10, errors) {
|
|
4441
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4442
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
const f = raw;
|
|
4446
|
+
requireNonEmptyString(f["id"], `${path10}.id`, errors);
|
|
4447
|
+
requireNonEmptyString(f["title"], `${path10}.title`, errors);
|
|
4448
|
+
requireNonEmptyString(f["description"], `${path10}.description`, errors);
|
|
4449
|
+
}
|
|
4450
|
+
function validateModel(raw, path10, errors) {
|
|
4451
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4452
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
const m = raw;
|
|
4456
|
+
requireNonEmptyString(m["name"], `${path10}.name`, errors);
|
|
4457
|
+
if (!Array.isArray(m["fields"])) {
|
|
4458
|
+
errors.push({ path: `${path10}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4459
|
+
} else {
|
|
4460
|
+
const fields = m["fields"];
|
|
4461
|
+
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4462
|
+
errors.push({ path: `${path10}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4463
|
+
}
|
|
4464
|
+
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4465
|
+
validateModelField(fields[j2], `${path10}.fields[${j2}]`, errors);
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
if (m["relations"] !== void 0) {
|
|
4469
|
+
if (!Array.isArray(m["relations"])) {
|
|
4470
|
+
errors.push({ path: `${path10}.relations`, message: "Must be an array of strings if present" });
|
|
4471
|
+
} else {
|
|
4472
|
+
const rels = m["relations"];
|
|
4473
|
+
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4474
|
+
if (typeof rels[j2] !== "string") {
|
|
4475
|
+
errors.push({ path: `${path10}.relations[${j2}]`, message: "Must be a string" });
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
function validateModelField(raw, path10, errors) {
|
|
4482
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4483
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
const f = raw;
|
|
4487
|
+
requireNonEmptyString(f["name"], `${path10}.name`, errors);
|
|
4488
|
+
requireNonEmptyString(f["type"], `${path10}.type`, errors);
|
|
4489
|
+
if (typeof f["required"] !== "boolean") {
|
|
4490
|
+
errors.push({ path: `${path10}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
function validateEndpoint(raw, path10, errors) {
|
|
4494
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4495
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
const e = raw;
|
|
4499
|
+
requireNonEmptyString(e["id"], `${path10}.id`, errors);
|
|
4500
|
+
requireNonEmptyString(e["description"], `${path10}.description`, errors);
|
|
4501
|
+
if (!VALID_METHODS.includes(e["method"])) {
|
|
4502
|
+
errors.push({
|
|
4503
|
+
path: `${path10}.method`,
|
|
4504
|
+
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4508
|
+
errors.push({
|
|
4509
|
+
path: `${path10}.path`,
|
|
4510
|
+
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
if (typeof e["auth"] !== "boolean") {
|
|
4514
|
+
errors.push({ path: `${path10}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4515
|
+
}
|
|
4516
|
+
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4517
|
+
errors.push({
|
|
4518
|
+
path: `${path10}.successStatus`,
|
|
4519
|
+
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
requireNonEmptyString(e["successDescription"], `${path10}.successDescription`, errors);
|
|
4523
|
+
if (e["request"] !== void 0) {
|
|
4524
|
+
validateRequestSchema(e["request"], `${path10}.request`, errors);
|
|
4525
|
+
}
|
|
4526
|
+
if (e["errors"] !== void 0) {
|
|
4527
|
+
if (!Array.isArray(e["errors"])) {
|
|
4528
|
+
errors.push({ path: `${path10}.errors`, message: "Must be an array if present" });
|
|
4529
|
+
} else {
|
|
4530
|
+
const errs = e["errors"];
|
|
4531
|
+
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4532
|
+
errors.push({ path: `${path10}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4533
|
+
}
|
|
4534
|
+
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4535
|
+
validateResponseError(errs[j2], `${path10}.errors[${j2}]`, errors);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
function validateRequestSchema(raw, path10, errors) {
|
|
4541
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4542
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4543
|
+
return;
|
|
4544
|
+
}
|
|
4545
|
+
const r = raw;
|
|
4546
|
+
for (const key of ["body", "query", "params"]) {
|
|
4547
|
+
if (r[key] !== void 0) {
|
|
4548
|
+
validateFieldMap(r[key], `${path10}.${key}`, errors);
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
function validateFieldMap(raw, path10, errors) {
|
|
4553
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4554
|
+
errors.push({ path: path10, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4557
|
+
const map = raw;
|
|
4558
|
+
for (const [k2, v2] of Object.entries(map)) {
|
|
4559
|
+
if (typeof v2 !== "string") {
|
|
4560
|
+
errors.push({ path: `${path10}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4564
|
+
function validateResponseError(raw, path10, errors) {
|
|
4565
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4566
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
const e = raw;
|
|
4570
|
+
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
4571
|
+
errors.push({ path: `${path10}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
4572
|
+
}
|
|
4573
|
+
requireNonEmptyString(e["code"], `${path10}.code`, errors);
|
|
4574
|
+
requireNonEmptyString(e["description"], `${path10}.description`, errors);
|
|
4575
|
+
}
|
|
4576
|
+
function validateBehavior(raw, path10, errors) {
|
|
4577
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4578
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
const b = raw;
|
|
4582
|
+
requireNonEmptyString(b["id"], `${path10}.id`, errors);
|
|
4583
|
+
requireNonEmptyString(b["description"], `${path10}.description`, errors);
|
|
4584
|
+
if (b["constraints"] !== void 0) {
|
|
4585
|
+
if (!Array.isArray(b["constraints"])) {
|
|
4586
|
+
errors.push({ path: `${path10}.constraints`, message: "Must be an array of strings if present" });
|
|
4587
|
+
} else {
|
|
4588
|
+
const cs2 = b["constraints"];
|
|
4589
|
+
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
4590
|
+
if (typeof cs2[j2] !== "string") {
|
|
4591
|
+
errors.push({ path: `${path10}.constraints[${j2}]`, message: "Must be a string" });
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
function validateComponent(raw, path10, errors) {
|
|
4598
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4599
|
+
errors.push({ path: path10, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4600
|
+
return;
|
|
4601
|
+
}
|
|
4602
|
+
const c = raw;
|
|
4603
|
+
requireNonEmptyString(c["id"], `${path10}.id`, errors);
|
|
4604
|
+
requireNonEmptyString(c["name"], `${path10}.name`, errors);
|
|
4605
|
+
requireNonEmptyString(c["description"], `${path10}.description`, errors);
|
|
4606
|
+
if (c["props"] !== void 0) {
|
|
4607
|
+
if (!Array.isArray(c["props"])) {
|
|
4608
|
+
errors.push({ path: `${path10}.props`, message: "Must be an array if present" });
|
|
4609
|
+
} else {
|
|
4610
|
+
const props = c["props"];
|
|
4611
|
+
for (let j2 = 0; j2 < props.length; j2++) {
|
|
4612
|
+
const p = props[j2];
|
|
4613
|
+
if (typeof p !== "object" || p === null) {
|
|
4614
|
+
errors.push({ path: `${path10}.props[${j2}]`, message: "Must be an object" });
|
|
4615
|
+
continue;
|
|
4616
|
+
}
|
|
4617
|
+
requireNonEmptyString(p["name"], `${path10}.props[${j2}].name`, errors);
|
|
4618
|
+
requireNonEmptyString(p["type"], `${path10}.props[${j2}].type`, errors);
|
|
4619
|
+
if (typeof p["required"] !== "boolean") {
|
|
4620
|
+
errors.push({ path: `${path10}.props[${j2}].required`, message: "Must be boolean" });
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
if (c["events"] !== void 0) {
|
|
4626
|
+
if (!Array.isArray(c["events"])) {
|
|
4627
|
+
errors.push({ path: `${path10}.events`, message: "Must be an array if present" });
|
|
4628
|
+
} else {
|
|
4629
|
+
const events = c["events"];
|
|
4630
|
+
for (let j2 = 0; j2 < events.length; j2++) {
|
|
4631
|
+
const e = events[j2];
|
|
4632
|
+
if (typeof e !== "object" || e === null) {
|
|
4633
|
+
errors.push({ path: `${path10}.events[${j2}]`, message: "Must be an object" });
|
|
4634
|
+
continue;
|
|
4635
|
+
}
|
|
4636
|
+
requireNonEmptyString(e["name"], `${path10}.events[${j2}].name`, errors);
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
if (c["state"] !== void 0) {
|
|
4641
|
+
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
4642
|
+
errors.push({ path: `${path10}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
if (c["apiCalls"] !== void 0) {
|
|
4646
|
+
if (!Array.isArray(c["apiCalls"])) {
|
|
4647
|
+
errors.push({ path: `${path10}.apiCalls`, message: "Must be an array of strings if present" });
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
function requireNonEmptyString(v2, path10, errors) {
|
|
4652
|
+
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
4653
|
+
errors.push({
|
|
4654
|
+
path: path10,
|
|
4655
|
+
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
function typeLabel(v2) {
|
|
4660
|
+
if (v2 === null) return "null";
|
|
4661
|
+
if (Array.isArray(v2)) return "array";
|
|
4662
|
+
return typeof v2;
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
// core/dsl-extractor.ts
|
|
4666
|
+
function dslFilePath(specFilePath) {
|
|
4667
|
+
const dir = path4.dirname(specFilePath);
|
|
4668
|
+
const base = path4.basename(specFilePath, ".md");
|
|
4669
|
+
return path4.join(dir, `${base}.dsl.json`);
|
|
4670
|
+
}
|
|
4671
|
+
function buildDslContextSection(dsl) {
|
|
4672
|
+
const lines = [
|
|
4673
|
+
"=== Feature DSL (structured summary \u2014 use for implementation guidance) ==="
|
|
4674
|
+
];
|
|
4675
|
+
if (dsl.models.length > 0) {
|
|
4676
|
+
lines.push("\n-- Data Models --");
|
|
4677
|
+
for (const model of dsl.models) {
|
|
4678
|
+
lines.push(`${model.name}:`);
|
|
4679
|
+
for (const field of model.fields) {
|
|
4680
|
+
const flags = [];
|
|
4681
|
+
if (field.required) flags.push("required");
|
|
4682
|
+
if (field.unique) flags.push("unique");
|
|
4683
|
+
lines.push(` ${field.name}: ${field.type}${flags.length ? ` (${flags.join(", ")})` : ""}`);
|
|
4684
|
+
}
|
|
4685
|
+
if (model.relations && model.relations.length > 0) {
|
|
4686
|
+
lines.push(` relations: ${model.relations.join("; ")}`);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
if (dsl.endpoints.length > 0) {
|
|
4691
|
+
lines.push("\n-- API Endpoints --");
|
|
4692
|
+
for (const ep of dsl.endpoints) {
|
|
4693
|
+
lines.push(`${ep.id}: ${ep.method} ${ep.path} [auth: ${ep.auth}] \u2192 ${ep.successStatus}`);
|
|
4694
|
+
lines.push(` ${ep.description}`);
|
|
4695
|
+
if (ep.request?.body) {
|
|
4696
|
+
const fields = Object.entries(ep.request.body).map(([k2, v2]) => `${k2}: ${v2}`).join(", ");
|
|
4697
|
+
lines.push(` body: { ${fields} }`);
|
|
4698
|
+
}
|
|
4699
|
+
if (ep.errors && ep.errors.length > 0) {
|
|
4700
|
+
lines.push(` errors: ${ep.errors.map((e) => `${e.status} ${e.code}`).join(", ")}`);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
if (dsl.behaviors.length > 0) {
|
|
4705
|
+
lines.push("\n-- Business Behaviors --");
|
|
4706
|
+
for (const b of dsl.behaviors) {
|
|
4707
|
+
lines.push(`${b.id}: ${b.description}`);
|
|
4708
|
+
if (b.trigger) lines.push(` trigger: ${b.trigger}`);
|
|
4709
|
+
if (b.constraints && b.constraints.length > 0) {
|
|
4710
|
+
lines.push(` rules: ${b.constraints.join("; ")}`);
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
if (dsl.components && dsl.components.length > 0) {
|
|
4715
|
+
lines.push("\n-- UI Components --");
|
|
4716
|
+
for (const cmp of dsl.components) {
|
|
4717
|
+
lines.push(`${cmp.id}: ${cmp.name} \u2014 ${cmp.description}`);
|
|
4718
|
+
if (cmp.props.length > 0) {
|
|
4719
|
+
lines.push(` props: ${cmp.props.map((p) => `${p.name}${p.required ? "" : "?"}:${p.type}`).join(", ")}`);
|
|
4720
|
+
}
|
|
4721
|
+
if (cmp.events.length > 0) {
|
|
4722
|
+
lines.push(` events: ${cmp.events.map((e) => `${e.name}(${e.payload ?? ""})`).join(", ")}`);
|
|
4723
|
+
}
|
|
4724
|
+
if (Object.keys(cmp.state).length > 0) {
|
|
4725
|
+
lines.push(` state: ${Object.entries(cmp.state).map(([k2, v2]) => `${k2}:${v2}`).join(", ")}`);
|
|
4726
|
+
}
|
|
4727
|
+
if (cmp.apiCalls.length > 0) {
|
|
4728
|
+
lines.push(` calls: ${cmp.apiCalls.join(", ")}`);
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
lines.push("\n=== End of DSL ===");
|
|
4733
|
+
return lines.join("\n");
|
|
4734
|
+
}
|
|
4735
|
+
async function loadDslForSpec(specFilePath) {
|
|
4736
|
+
const dslPath = dslFilePath(specFilePath);
|
|
4737
|
+
if (!await fs6.pathExists(dslPath)) return null;
|
|
4738
|
+
try {
|
|
4739
|
+
const raw = await fs6.readJson(dslPath);
|
|
4740
|
+
const result = validateDsl(raw);
|
|
4741
|
+
return result.valid ? result.dsl : null;
|
|
4742
|
+
} catch {
|
|
4743
|
+
return null;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
// core/frontend-context-loader.ts
|
|
4748
|
+
import * as fs7 from "fs-extra";
|
|
4749
|
+
import * as path5 from "path";
|
|
4750
|
+
var STATE_MANAGEMENT_LIBS = [
|
|
4751
|
+
"zustand",
|
|
4752
|
+
"redux",
|
|
4753
|
+
"@reduxjs/toolkit",
|
|
4754
|
+
"jotai",
|
|
4755
|
+
"recoil",
|
|
4756
|
+
"mobx",
|
|
4757
|
+
"mobx-react",
|
|
4758
|
+
"valtio",
|
|
4759
|
+
"pinia",
|
|
4760
|
+
"vuex"
|
|
4761
|
+
];
|
|
4762
|
+
var HTTP_CLIENT_LIBS = [
|
|
4763
|
+
["swr", "swr"],
|
|
4764
|
+
["@tanstack/react-query", "react-query"],
|
|
4765
|
+
["react-query", "react-query"],
|
|
4766
|
+
["axios", "axios"],
|
|
4767
|
+
["ky", "ky"]
|
|
4768
|
+
];
|
|
4769
|
+
var UI_LIBRARY_LIBS = [
|
|
4770
|
+
["antd", "antd"],
|
|
4771
|
+
["@ant-design/pro-components", "antd-pro"],
|
|
4772
|
+
["@mui/material", "mui"],
|
|
4773
|
+
["@chakra-ui/react", "chakra-ui"],
|
|
4774
|
+
["shadcn-ui", "shadcn"],
|
|
4775
|
+
["@radix-ui/react-primitive", "radix-ui"],
|
|
4776
|
+
["element-plus", "element-plus"],
|
|
4777
|
+
["vant", "vant"],
|
|
4778
|
+
["tailwindcss", "tailwind"],
|
|
4779
|
+
["@tailwindcss/vite", "tailwind"],
|
|
4780
|
+
["react-native-paper", "react-native-paper"]
|
|
4781
|
+
];
|
|
4782
|
+
var ROUTING_LIBS = [
|
|
4783
|
+
["react-router-dom", "react-router"],
|
|
4784
|
+
["react-router", "react-router"],
|
|
4785
|
+
["@tanstack/react-router", "tanstack-router"],
|
|
4786
|
+
["react-navigation", "react-navigation"],
|
|
4787
|
+
["expo-router", "expo-router"],
|
|
4788
|
+
["vue-router", "vue-router"]
|
|
4789
|
+
];
|
|
4790
|
+
async function loadFrontendContext(projectRoot) {
|
|
4791
|
+
const ctx = {
|
|
4792
|
+
framework: "unknown",
|
|
4793
|
+
stateManagement: [],
|
|
4794
|
+
httpClient: "fetch",
|
|
4795
|
+
uiLibrary: "unknown",
|
|
4796
|
+
routingPattern: "unknown",
|
|
4797
|
+
testFramework: "unknown",
|
|
4798
|
+
existingApiFiles: [],
|
|
4799
|
+
apiWrapperContent: [],
|
|
4800
|
+
hookFiles: [],
|
|
4801
|
+
hookPatterns: [],
|
|
4802
|
+
storeFiles: [],
|
|
4803
|
+
storePatterns: [],
|
|
4804
|
+
reusableComponents: [],
|
|
4805
|
+
pageExamples: [],
|
|
4806
|
+
componentPatterns: []
|
|
4807
|
+
};
|
|
4808
|
+
try {
|
|
4809
|
+
const pkgPath = path5.join(projectRoot, "package.json");
|
|
4810
|
+
if (!await fs7.pathExists(pkgPath)) return ctx;
|
|
4811
|
+
const pkg = await fs7.readJson(pkgPath);
|
|
4812
|
+
const allDeps = {
|
|
4813
|
+
...pkg.dependencies ?? {},
|
|
4814
|
+
...pkg.devDependencies ?? {}
|
|
4815
|
+
};
|
|
4816
|
+
const depKeys = Object.keys(allDeps);
|
|
4817
|
+
const has = (name) => depKeys.includes(name);
|
|
4818
|
+
if (has("react-native") || has("expo")) {
|
|
4819
|
+
ctx.framework = "react-native";
|
|
4820
|
+
} else if (has("next")) {
|
|
4821
|
+
ctx.framework = "next";
|
|
4822
|
+
} else if (has("react")) {
|
|
4823
|
+
ctx.framework = "react";
|
|
4824
|
+
} else if (has("vue")) {
|
|
4825
|
+
ctx.framework = "vue";
|
|
4826
|
+
}
|
|
4827
|
+
ctx.stateManagement = STATE_MANAGEMENT_LIBS.filter((lib) => has(lib));
|
|
4828
|
+
for (const [lib, label] of HTTP_CLIENT_LIBS) {
|
|
4829
|
+
if (has(lib)) {
|
|
4830
|
+
ctx.httpClient = label;
|
|
4831
|
+
break;
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
for (const [lib, label] of UI_LIBRARY_LIBS) {
|
|
4835
|
+
if (has(lib)) {
|
|
4836
|
+
ctx.uiLibrary = label;
|
|
4837
|
+
break;
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
if (ctx.uiLibrary === "unknown") {
|
|
4841
|
+
ctx.uiLibrary = "none";
|
|
4842
|
+
}
|
|
4843
|
+
if (ctx.framework === "next") {
|
|
4844
|
+
const hasAppDir = await fs7.pathExists(path5.join(projectRoot, "app"));
|
|
4845
|
+
ctx.routingPattern = hasAppDir ? "next-app-router" : "next-pages-router";
|
|
4846
|
+
} else {
|
|
4847
|
+
for (const [lib, label] of ROUTING_LIBS) {
|
|
4848
|
+
if (has(lib)) {
|
|
4849
|
+
ctx.routingPattern = label;
|
|
4850
|
+
break;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
if (depKeys.includes("@testing-library/react") || depKeys.includes("@testing-library/vue")) {
|
|
4855
|
+
ctx.testFramework = "rtl";
|
|
4856
|
+
} else if (depKeys.includes("cypress")) {
|
|
4857
|
+
ctx.testFramework = "cypress";
|
|
4858
|
+
} else if (depKeys.includes("vitest")) {
|
|
4859
|
+
ctx.testFramework = "vitest";
|
|
4860
|
+
} else if (depKeys.includes("jest") || depKeys.includes("@jest/core")) {
|
|
4861
|
+
ctx.testFramework = "jest";
|
|
4862
|
+
}
|
|
4863
|
+
const apiFilePatterns = [
|
|
4864
|
+
"src/api/**/*.{ts,js}",
|
|
4865
|
+
"src/apis/**/*.{ts,js}",
|
|
4866
|
+
"src/services/**/*.{ts,js}",
|
|
4867
|
+
"src/lib/api/**/*.{ts,js}",
|
|
4868
|
+
"src/utils/api/**/*.{ts,js}",
|
|
4869
|
+
"api/**/*.{ts,js}",
|
|
4870
|
+
"services/**/*.{ts,js}"
|
|
4871
|
+
];
|
|
4872
|
+
for (const pattern of apiFilePatterns) {
|
|
4873
|
+
const files = await Ze(pattern, {
|
|
4874
|
+
cwd: projectRoot,
|
|
4875
|
+
ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"]
|
|
4876
|
+
});
|
|
4877
|
+
ctx.existingApiFiles.push(...files);
|
|
4878
|
+
}
|
|
4879
|
+
ctx.existingApiFiles = [...new Set(ctx.existingApiFiles)].slice(0, 20);
|
|
4880
|
+
for (const relPath of ctx.existingApiFiles.slice(0, 2)) {
|
|
4881
|
+
try {
|
|
4882
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
4883
|
+
const preview = content.split("\n").slice(0, 60).join("\n");
|
|
4884
|
+
ctx.apiWrapperContent.push(`// ${relPath}
|
|
4885
|
+
${preview}`);
|
|
4886
|
+
} catch {
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
const hookPatterns = [
|
|
4890
|
+
"src/hooks/use*.{ts,tsx}",
|
|
4891
|
+
"src/**/hooks/use*.{ts,tsx}",
|
|
4892
|
+
"hooks/use*.{ts,tsx}"
|
|
4893
|
+
];
|
|
4894
|
+
for (const pattern of hookPatterns) {
|
|
4895
|
+
const files = await Ze(pattern, {
|
|
4896
|
+
cwd: projectRoot,
|
|
4897
|
+
ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"]
|
|
4898
|
+
});
|
|
4899
|
+
ctx.hookFiles.push(...files);
|
|
4900
|
+
}
|
|
4901
|
+
ctx.hookFiles = [...new Set(ctx.hookFiles)].slice(0, 15);
|
|
4902
|
+
for (const relPath of ctx.hookFiles.slice(0, 2)) {
|
|
4903
|
+
try {
|
|
4904
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
4905
|
+
const preview = content.split("\n").slice(0, 30).join("\n");
|
|
4906
|
+
ctx.hookPatterns.push(`// ${relPath}
|
|
4907
|
+
${preview}`);
|
|
4908
|
+
} catch {
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
const storeFilePatterns = [
|
|
4912
|
+
"src/store/**/*.{ts,js}",
|
|
4913
|
+
"src/stores/**/*.{ts,js}",
|
|
4914
|
+
"src/**/slice*.{ts,js}",
|
|
4915
|
+
"src/**/*slice.{ts,js}",
|
|
4916
|
+
"src/**/*store.{ts,js}",
|
|
4917
|
+
"src/**/*Store.{ts,js}",
|
|
4918
|
+
"store/**/*.{ts,js}",
|
|
4919
|
+
"stores/**/*.{ts,js}"
|
|
4920
|
+
];
|
|
4921
|
+
for (const pattern of storeFilePatterns) {
|
|
4922
|
+
const files = await Ze(pattern, {
|
|
4923
|
+
cwd: projectRoot,
|
|
4924
|
+
ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"]
|
|
4925
|
+
});
|
|
4926
|
+
ctx.storeFiles.push(...files);
|
|
4927
|
+
}
|
|
4928
|
+
ctx.storeFiles = [...new Set(ctx.storeFiles)].slice(0, 10);
|
|
4929
|
+
for (const relPath of ctx.storeFiles.slice(0, 2)) {
|
|
4930
|
+
try {
|
|
4931
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
4932
|
+
const preview = content.split("\n").slice(0, 60).join("\n");
|
|
4933
|
+
ctx.storePatterns.push(`// ${relPath}
|
|
4934
|
+
${preview}`);
|
|
4935
|
+
} catch {
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
const httpImportRegex = /^import\s+(?:\w+|\{[^}]+\})\s+from\s+['"](@\/[^'"]+|axios|ky)['"]/m;
|
|
4939
|
+
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
4940
|
+
try {
|
|
4941
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
4942
|
+
const match = content.match(httpImportRegex);
|
|
4943
|
+
if (match) {
|
|
4944
|
+
ctx.httpClientImport = match[0].trim();
|
|
4945
|
+
break;
|
|
4946
|
+
}
|
|
4947
|
+
} catch {
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
const paginationFieldNames = ["pageIndex", "pageSize", "pageNum", "current", "page", "size", "offset", "limit"];
|
|
4951
|
+
const paginationInterfaceRegex = new RegExp(
|
|
4952
|
+
`(?:interface|type)\\s+(\\w*(?:Params|Query|Request|Filter|Page)\\w*)\\s*\\{[^}]*\\b(?:${paginationFieldNames.join("|")})\\b[^}]*\\}`,
|
|
4953
|
+
"s"
|
|
4954
|
+
);
|
|
4955
|
+
const apiExportFnRegex = /export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)[^{]*\{[\s\S]*?\n\}/g;
|
|
4956
|
+
for (const relPath of ctx.existingApiFiles) {
|
|
4957
|
+
if (/types?\.ts$|index\.ts$/.test(relPath)) continue;
|
|
4958
|
+
try {
|
|
4959
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
4960
|
+
if (!paginationFieldNames.some((f) => content.includes(f))) continue;
|
|
4961
|
+
const interfaceMatch = content.match(paginationInterfaceRegex);
|
|
4962
|
+
if (!interfaceMatch) continue;
|
|
4963
|
+
const interfaceName = interfaceMatch[1];
|
|
4964
|
+
const fnRegex = new RegExp(
|
|
4965
|
+
`export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${interfaceName}[^)]*\\)[\\s\\S]*?\\n\\}`,
|
|
4966
|
+
""
|
|
4967
|
+
);
|
|
4968
|
+
const fnMatch = content.match(fnRegex);
|
|
4969
|
+
if (fnMatch) {
|
|
4970
|
+
ctx.paginationExample = `// From ${relPath}
|
|
4971
|
+
${interfaceMatch[0]}
|
|
4972
|
+
|
|
4973
|
+
${fnMatch[0]}`;
|
|
4974
|
+
break;
|
|
4975
|
+
}
|
|
4976
|
+
} catch {
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
const sharedComponentDirs = ["src/components", "components"];
|
|
4980
|
+
for (const dir of sharedComponentDirs) {
|
|
4981
|
+
const absDir = path5.join(projectRoot, dir);
|
|
4982
|
+
if (!await fs7.pathExists(absDir)) continue;
|
|
4983
|
+
const files = await Ze("**/*.{vue,tsx,jsx}", {
|
|
4984
|
+
cwd: absDir,
|
|
4985
|
+
ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"],
|
|
4986
|
+
maxDepth: 4
|
|
4987
|
+
});
|
|
4988
|
+
ctx.reusableComponents.push(...files.map((f) => path5.join(dir, f)));
|
|
4989
|
+
}
|
|
4990
|
+
ctx.reusableComponents = [...new Set(ctx.reusableComponents)].slice(0, 40);
|
|
4991
|
+
const viewDirs = ["src/views", "src/pages", "views", "pages"];
|
|
4992
|
+
const viewFiles = [];
|
|
4993
|
+
for (const dir of viewDirs) {
|
|
4994
|
+
const absDir = path5.join(projectRoot, dir);
|
|
4995
|
+
if (!await fs7.pathExists(absDir)) continue;
|
|
4996
|
+
const files = await Ze("**/*.{vue,tsx,jsx}", {
|
|
4997
|
+
cwd: absDir,
|
|
4998
|
+
ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"],
|
|
4999
|
+
maxDepth: 3
|
|
5000
|
+
});
|
|
5001
|
+
viewFiles.push(...files.map((f) => path5.join(dir, f)));
|
|
5002
|
+
if (viewFiles.length >= 6) break;
|
|
5003
|
+
}
|
|
5004
|
+
for (const relPath of viewFiles.slice(0, 2)) {
|
|
5005
|
+
try {
|
|
5006
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
5007
|
+
const preview = content.split("\n").slice(0, 80).join("\n");
|
|
5008
|
+
ctx.pageExamples.push(`// ${relPath}
|
|
5009
|
+
${preview}`);
|
|
5010
|
+
} catch {
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
const componentFiles = [];
|
|
5014
|
+
for (const dir of sharedComponentDirs) {
|
|
5015
|
+
const absDir = path5.join(projectRoot, dir);
|
|
5016
|
+
if (!await fs7.pathExists(absDir)) continue;
|
|
5017
|
+
const files = await Ze("**/*.{tsx,vue,jsx}", {
|
|
5018
|
+
cwd: absDir,
|
|
5019
|
+
ignore: ["**/*.test.*", "**/*.spec.*", "node_modules/**"],
|
|
5020
|
+
maxDepth: 2
|
|
5021
|
+
});
|
|
5022
|
+
componentFiles.push(...files.map((f) => path5.join(dir, f)));
|
|
5023
|
+
if (componentFiles.length >= 5) break;
|
|
5024
|
+
}
|
|
5025
|
+
for (const relPath of componentFiles.slice(0, 3)) {
|
|
5026
|
+
try {
|
|
5027
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
5028
|
+
const preview = content.split("\n").slice(0, 40).join("\n");
|
|
5029
|
+
ctx.componentPatterns.push(`// ${relPath}
|
|
5030
|
+
${preview}`);
|
|
5031
|
+
} catch {
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
await extractRouteModuleContext(projectRoot, ctx);
|
|
5035
|
+
} catch {
|
|
5036
|
+
}
|
|
5037
|
+
return ctx;
|
|
5038
|
+
}
|
|
5039
|
+
async function extractRouteModuleContext(projectRoot, ctx) {
|
|
5040
|
+
const modulePatterns = [
|
|
5041
|
+
"src/router/modules/**/*.{ts,js}",
|
|
5042
|
+
"src/routes/modules/**/*.{ts,js}",
|
|
5043
|
+
"src/router/**/*.{ts,js}"
|
|
5044
|
+
];
|
|
5045
|
+
const moduleFiles = [];
|
|
5046
|
+
for (const pattern of modulePatterns) {
|
|
5047
|
+
const files = await Ze(pattern, {
|
|
5048
|
+
cwd: projectRoot,
|
|
5049
|
+
ignore: ["**/index.{ts,js}", "node_modules/**", "**/*.test.*"]
|
|
5050
|
+
});
|
|
5051
|
+
moduleFiles.push(...files);
|
|
5052
|
+
}
|
|
5053
|
+
if (moduleFiles.length === 0) return;
|
|
5054
|
+
const layoutImportRegex = /^(?:const\s+Layout\s*=.*import\(['"][^'"]+['"]\)|import\s+Layout\s+from\s+['"][^'"]+['"])/m;
|
|
5055
|
+
for (const relPath of moduleFiles) {
|
|
5056
|
+
try {
|
|
5057
|
+
const content = await fs7.readFile(path5.join(projectRoot, relPath), "utf-8");
|
|
5058
|
+
const match = content.match(layoutImportRegex);
|
|
5059
|
+
if (match) {
|
|
5060
|
+
ctx.layoutImport = match[0].trim();
|
|
5061
|
+
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
5062
|
+
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
5063
|
+
break;
|
|
5064
|
+
}
|
|
5065
|
+
} catch {
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
function buildFrontendContextSection(ctx) {
|
|
5070
|
+
const lines = [
|
|
5071
|
+
"=== Frontend Project Context ===",
|
|
5072
|
+
`Framework : ${ctx.framework}`,
|
|
5073
|
+
`State Management : ${ctx.stateManagement.join(", ") || "none detected"}`,
|
|
5074
|
+
`HTTP Client : ${ctx.httpClient}`,
|
|
5075
|
+
`UI Library : ${ctx.uiLibrary}`,
|
|
5076
|
+
`Routing : ${ctx.routingPattern}`,
|
|
5077
|
+
`Test Framework : ${ctx.testFramework}`
|
|
5078
|
+
];
|
|
5079
|
+
if (ctx.layoutImport) {
|
|
5080
|
+
lines.push(
|
|
5081
|
+
`
|
|
5082
|
+
Layout component import (COPY THIS EXACTLY in every new route module \u2014 do NOT invent a different path):`,
|
|
5083
|
+
` ${ctx.layoutImport}`
|
|
5084
|
+
);
|
|
5085
|
+
}
|
|
5086
|
+
if (ctx.routeModuleExample) {
|
|
5087
|
+
lines.push(
|
|
5088
|
+
`
|
|
5089
|
+
Existing route module template (${ctx.routeModuleExample.path}) \u2014 use this as the structural template for new route modules:`,
|
|
5090
|
+
"```",
|
|
5091
|
+
ctx.routeModuleExample.content,
|
|
5092
|
+
"```"
|
|
5093
|
+
);
|
|
5094
|
+
}
|
|
5095
|
+
if (ctx.existingApiFiles.length > 0) {
|
|
5096
|
+
lines.push(`
|
|
5097
|
+
Existing API/service files (${ctx.existingApiFiles.length}):`);
|
|
5098
|
+
ctx.existingApiFiles.slice(0, 10).forEach((f) => lines.push(` - ${f}`));
|
|
5099
|
+
}
|
|
5100
|
+
if (ctx.httpClientImport) {
|
|
5101
|
+
lines.push(
|
|
5102
|
+
`
|
|
5103
|
+
HTTP client import (COPY THIS EXACTLY in every new API file \u2014 do NOT import from any other path):`,
|
|
5104
|
+
` ${ctx.httpClientImport}`
|
|
5105
|
+
);
|
|
5106
|
+
}
|
|
5107
|
+
if (ctx.paginationExample) {
|
|
5108
|
+
lines.push(
|
|
5109
|
+
`
|
|
5110
|
+
Pagination pattern (COPY THIS EXACTLY for all paginated list APIs \u2014 use IDENTICAL parameter names, HTTP method, and call style):`,
|
|
5111
|
+
"```typescript",
|
|
5112
|
+
ctx.paginationExample,
|
|
5113
|
+
"```"
|
|
5114
|
+
);
|
|
5115
|
+
}
|
|
5116
|
+
if (ctx.apiWrapperContent.length > 0) {
|
|
5117
|
+
lines.push(`
|
|
5118
|
+
API file patterns (new API functions must follow this exact structure):`);
|
|
5119
|
+
ctx.apiWrapperContent.forEach((p) => {
|
|
5120
|
+
lines.push("```");
|
|
5121
|
+
lines.push(p);
|
|
5122
|
+
lines.push("```");
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
if (ctx.hookFiles.length > 0) {
|
|
5126
|
+
lines.push(`
|
|
5127
|
+
Existing custom hooks (${ctx.hookFiles.length}):`);
|
|
5128
|
+
ctx.hookFiles.slice(0, 8).forEach((f) => lines.push(` - ${f}`));
|
|
5129
|
+
}
|
|
5130
|
+
if (ctx.hookPatterns.length > 0) {
|
|
5131
|
+
lines.push(`
|
|
5132
|
+
Hook patterns (follow same structure):`);
|
|
5133
|
+
ctx.hookPatterns.forEach((p) => {
|
|
5134
|
+
lines.push("```");
|
|
5135
|
+
lines.push(p);
|
|
5136
|
+
lines.push("```");
|
|
5137
|
+
});
|
|
5138
|
+
}
|
|
5139
|
+
if (ctx.storeFiles.length > 0) {
|
|
5140
|
+
lines.push(`
|
|
5141
|
+
State store files (${ctx.storeFiles.length}):`);
|
|
5142
|
+
ctx.storeFiles.slice(0, 8).forEach((f) => lines.push(` - ${f}`));
|
|
5143
|
+
}
|
|
5144
|
+
if (ctx.storePatterns.length > 0) {
|
|
5145
|
+
lines.push(
|
|
5146
|
+
`
|
|
5147
|
+
Existing store patterns (CRITICAL \u2014 stores in this project call API layer functions, they do NOT make HTTP requests directly):`,
|
|
5148
|
+
`Follow this exact structure for new stores:`
|
|
5149
|
+
);
|
|
5150
|
+
ctx.storePatterns.forEach((p) => {
|
|
5151
|
+
lines.push("```");
|
|
5152
|
+
lines.push(p);
|
|
5153
|
+
lines.push("```");
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
if (ctx.reusableComponents.length > 0) {
|
|
5157
|
+
lines.push(
|
|
5158
|
+
`
|
|
5159
|
+
Existing reusable components in src/components/ (${ctx.reusableComponents.length} files):`,
|
|
5160
|
+
`ALWAYS check this list before creating a new component. Import and reuse existing ones instead of reinventing.`
|
|
5161
|
+
);
|
|
5162
|
+
ctx.reusableComponents.forEach((f) => lines.push(` - ${f}`));
|
|
5163
|
+
}
|
|
5164
|
+
if (ctx.pageExamples.length > 0) {
|
|
5165
|
+
lines.push(
|
|
5166
|
+
`
|
|
5167
|
+
Existing page examples (shows which UI library components and shared components are used \u2014 follow the same import and usage patterns):`
|
|
5168
|
+
);
|
|
5169
|
+
ctx.pageExamples.forEach((p) => {
|
|
5170
|
+
lines.push("```");
|
|
5171
|
+
lines.push(p);
|
|
5172
|
+
lines.push("```");
|
|
5173
|
+
});
|
|
5174
|
+
}
|
|
5175
|
+
if (ctx.componentPatterns.length > 0) {
|
|
5176
|
+
lines.push(`
|
|
5177
|
+
Shared component structure patterns:`);
|
|
5178
|
+
ctx.componentPatterns.forEach((p) => {
|
|
5179
|
+
lines.push("```");
|
|
5180
|
+
lines.push(p.slice(0, 500));
|
|
5181
|
+
lines.push("```");
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
5184
|
+
lines.push("=== End of Frontend Context ===");
|
|
5185
|
+
return lines.join("\n");
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
// core/code-generator.ts
|
|
5189
|
+
function buildSharedConfigSection(context) {
|
|
5190
|
+
if (!context?.sharedConfigFiles || context.sharedConfigFiles.length === 0) return "";
|
|
5191
|
+
const lines = [
|
|
5192
|
+
"\n=== Existing Shared Config Files (study these to learn project conventions) ===",
|
|
5193
|
+
"These are real files from the project. Use them as ground truth for naming, structure, and registration patterns.",
|
|
5194
|
+
"Modify them in-place when adding new entries. Do NOT create parallel files for the same purpose.\n"
|
|
5195
|
+
];
|
|
5196
|
+
for (const f of context.sharedConfigFiles) {
|
|
5197
|
+
lines.push(`--- File: ${f.path} [${f.category}] ---`);
|
|
5198
|
+
lines.push(f.preview);
|
|
5199
|
+
lines.push("");
|
|
5200
|
+
}
|
|
5201
|
+
return lines.join("\n") + "\n";
|
|
5202
|
+
}
|
|
5203
|
+
function buildInstalledPackagesSection(context) {
|
|
5204
|
+
if (!context?.dependencies || context.dependencies.length === 0) return "";
|
|
5205
|
+
return `
|
|
5206
|
+
=== Installed Packages (ONLY use packages from this list \u2014 NEVER import anything not listed here) ===
|
|
5207
|
+
${context.dependencies.join(", ")}
|
|
5208
|
+
`;
|
|
5209
|
+
}
|
|
5210
|
+
function buildGeneratedFilesSection(cache) {
|
|
5211
|
+
if (cache.size === 0) return "";
|
|
5212
|
+
const lines = [
|
|
5213
|
+
"\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ==="
|
|
5214
|
+
];
|
|
5215
|
+
for (const [filePath, content] of cache) {
|
|
5216
|
+
lines.push(`
|
|
5217
|
+
--- ${filePath} ---`);
|
|
5218
|
+
lines.push(content.slice(0, 800));
|
|
5219
|
+
if (content.length > 800) lines.push("... (truncated)");
|
|
5220
|
+
}
|
|
5221
|
+
return lines.join("\n") + "\n";
|
|
5222
|
+
}
|
|
5223
|
+
function isRtkAvailable() {
|
|
5224
|
+
try {
|
|
5225
|
+
execSync("rtk --version", { stdio: "ignore" });
|
|
5226
|
+
return true;
|
|
5227
|
+
} catch {
|
|
5228
|
+
return false;
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
function stripCodeFences(output) {
|
|
5232
|
+
const fenced = output.match(/^```(?:\w+)?\n([\s\S]*?)```\s*$/m);
|
|
5233
|
+
if (fenced) return fenced[1].trim();
|
|
5234
|
+
const lines = output.split("\n");
|
|
5235
|
+
if (lines[0].startsWith("```")) lines.shift();
|
|
5236
|
+
if (lines[lines.length - 1].trim() === "```") lines.pop();
|
|
5237
|
+
return lines.join("\n").trim();
|
|
5238
|
+
}
|
|
5239
|
+
function parseJsonArray(text) {
|
|
5240
|
+
const fenced = text.match(/```(?:json)?\n(\[[\s\S]*?\])\n```/);
|
|
5241
|
+
const raw = fenced ? fenced[1] : text.match(/\[[\s\S]*?\]/)?.[0] ?? "";
|
|
5242
|
+
try {
|
|
5243
|
+
const parsed = JSON.parse(raw);
|
|
5244
|
+
if (Array.isArray(parsed)) return parsed;
|
|
5245
|
+
} catch {
|
|
5246
|
+
}
|
|
5247
|
+
return [];
|
|
5248
|
+
}
|
|
5249
|
+
var CodeGenerator = class {
|
|
5250
|
+
constructor(provider, mode = "claude-code") {
|
|
5251
|
+
this.provider = provider;
|
|
5252
|
+
this.mode = mode;
|
|
5253
|
+
}
|
|
5254
|
+
/** Returns the list of file paths written to disk (useful for api-mode review). */
|
|
5255
|
+
async generateCode(specFilePath, workingDir, context, options = {}) {
|
|
5256
|
+
let effectiveMode = this.mode;
|
|
5257
|
+
if (effectiveMode === "claude-code" && this.provider.providerName !== "claude") {
|
|
5258
|
+
console.log(
|
|
5259
|
+
chalk6.yellow(
|
|
5260
|
+
`
|
|
5261
|
+
\u26A0 codegen \u6A21\u5F0F "claude-code" \u9700\u8981 Claude\uFF0C\u4F46\u5F53\u524D provider \u662F "${this.provider.providerName}"\u3002`
|
|
5262
|
+
)
|
|
5263
|
+
);
|
|
5264
|
+
console.log(chalk6.gray(` \u81EA\u52A8\u5207\u6362\u5230 "api" \u6A21\u5F0F\uFF08\u4F7F\u7528 ${this.provider.providerName}/${this.provider.modelName} \u751F\u6210\u4EE3\u7801\uFF09\u3002`));
|
|
5265
|
+
console.log(chalk6.gray(` \u63D0\u793A\uFF1A\u8FD0\u884C \`ai-spec config --codegen api\` \u53EF\u56FA\u5316\u6B64\u8BBE\u7F6E\u3002
|
|
5266
|
+
`));
|
|
5267
|
+
effectiveMode = "api";
|
|
5268
|
+
}
|
|
5269
|
+
switch (effectiveMode) {
|
|
5270
|
+
case "claude-code":
|
|
5271
|
+
await this.runClaudeCode(specFilePath, workingDir, options);
|
|
5272
|
+
return [];
|
|
5273
|
+
case "api":
|
|
5274
|
+
return this.runApiMode(specFilePath, workingDir, context, options);
|
|
5275
|
+
case "plan":
|
|
5276
|
+
await this.runPlanMode(specFilePath);
|
|
5277
|
+
return [];
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
// ── Mode: claude-code ──────────────────────────────────────────────────────
|
|
5281
|
+
isClaudeCLIAvailable() {
|
|
5282
|
+
try {
|
|
5283
|
+
execSync("claude --version", { stdio: "ignore" });
|
|
5284
|
+
return true;
|
|
5285
|
+
} catch {
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
async runClaudeCode(specFilePath, workingDir, options = {}) {
|
|
5290
|
+
console.log(chalk6.blue("\n\u2500\u2500\u2500 Code Generation: Claude Code CLI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5291
|
+
if (!this.isClaudeCLIAvailable()) {
|
|
5292
|
+
console.log(chalk6.yellow(" \u26A0\uFE0F Claude Code CLI not found. Falling back to plan mode."));
|
|
5293
|
+
console.log(chalk6.gray(" Install: npm install -g @anthropic-ai/claude-code"));
|
|
3737
5294
|
return this.runPlanMode(specFilePath);
|
|
3738
5295
|
}
|
|
5296
|
+
const rtkAvailable = isRtkAvailable();
|
|
5297
|
+
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
5298
|
+
if (rtkAvailable) {
|
|
5299
|
+
console.log(chalk6.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
5300
|
+
}
|
|
3739
5301
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
5302
|
+
if (options.auto && tasks && tasks.length > 0) {
|
|
5303
|
+
return this.runClaudeCodeIncremental(tasks, specFilePath, workingDir, claudeCmd, options);
|
|
5304
|
+
}
|
|
3740
5305
|
const taskSection = tasks && tasks.length > 0 ? `
|
|
3741
5306
|
|
|
3742
5307
|
== Implementation Tasks (implement in order) ==
|
|
@@ -3744,64 +5309,139 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
3744
5309
|
Files: ${t.filesToTouch.join(", ")}
|
|
3745
5310
|
Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
|
|
3746
5311
|
const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
|
|
3747
|
-
const promptFile =
|
|
3748
|
-
await
|
|
3749
|
-
const rtkAvailable = isRtkAvailable();
|
|
3750
|
-
const claudeCmd = rtkAvailable ? "rtk claude" : "claude";
|
|
3751
|
-
if (rtkAvailable) {
|
|
3752
|
-
console.log(chalk3.green(" \u2713 RTK detected \u2014 using rtk claude for token savings"));
|
|
3753
|
-
}
|
|
5312
|
+
const promptFile = path6.join(workingDir, ".claude-prompt.txt");
|
|
5313
|
+
await fs8.writeFile(promptFile, promptContent, "utf-8");
|
|
3754
5314
|
if (options.auto) {
|
|
3755
|
-
console.log(
|
|
3756
|
-
console.log(
|
|
3757
|
-
if (tasks) console.log(chalk3.gray(` Tasks: ${tasks.length} tasks loaded`));
|
|
5315
|
+
console.log(chalk6.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
5316
|
+
console.log(chalk6.gray(` Spec: ${specFilePath}`));
|
|
3758
5317
|
try {
|
|
3759
5318
|
execSync(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
|
|
3760
5319
|
cwd: workingDir,
|
|
3761
5320
|
stdio: "inherit"
|
|
3762
5321
|
});
|
|
3763
|
-
console.log(
|
|
5322
|
+
console.log(chalk6.green("\n \u2714 Claude Code completed."));
|
|
3764
5323
|
} catch {
|
|
3765
|
-
console.log(
|
|
5324
|
+
console.log(chalk6.yellow("\n Claude Code exited. Check output above."));
|
|
3766
5325
|
}
|
|
3767
5326
|
} else {
|
|
3768
|
-
console.log(
|
|
3769
|
-
console.log(
|
|
3770
|
-
if (tasks) console.log(
|
|
3771
|
-
console.log(
|
|
5327
|
+
console.log(chalk6.cyan(` \u{1F680} Launching ${claudeCmd} in: ${workingDir}`));
|
|
5328
|
+
console.log(chalk6.gray(` Spec: ${specFilePath}`));
|
|
5329
|
+
if (tasks) console.log(chalk6.gray(` Tasks: ${tasks.length} tasks loaded into .claude-prompt.txt`));
|
|
5330
|
+
console.log(chalk6.gray(" Prompt pre-loaded in .claude-prompt.txt\n"));
|
|
3772
5331
|
try {
|
|
3773
5332
|
execSync(claudeCmd, { cwd: workingDir, stdio: "inherit" });
|
|
3774
|
-
console.log(
|
|
5333
|
+
console.log(chalk6.green("\n \u2714 Claude Code session completed."));
|
|
3775
5334
|
} catch {
|
|
3776
|
-
console.log(
|
|
5335
|
+
console.log(chalk6.yellow("\n Claude Code session ended. Continuing workflow."));
|
|
3777
5336
|
}
|
|
3778
5337
|
}
|
|
3779
5338
|
}
|
|
5339
|
+
/**
|
|
5340
|
+
* Incremental claude-code execution: one `claude -p` call per task.
|
|
5341
|
+
* Tasks marked as "done" are skipped (resume support).
|
|
5342
|
+
* Progress is shown as a percentage bar.
|
|
5343
|
+
*/
|
|
5344
|
+
async runClaudeCodeIncremental(tasks, specFilePath, workingDir, claudeCmd, options) {
|
|
5345
|
+
const pending = tasks.filter((t) => t.status !== "done");
|
|
5346
|
+
const doneCount = tasks.length - pending.length;
|
|
5347
|
+
if (options.resume && doneCount > 0) {
|
|
5348
|
+
console.log(chalk6.cyan(`
|
|
5349
|
+
Resuming: ${doneCount}/${tasks.length} tasks already done \u2014 skipping.`));
|
|
5350
|
+
} else {
|
|
5351
|
+
console.log(chalk6.cyan(`
|
|
5352
|
+
Incremental mode: ${tasks.length} tasks`));
|
|
5353
|
+
}
|
|
5354
|
+
let completed = doneCount;
|
|
5355
|
+
for (const task of tasks) {
|
|
5356
|
+
if (task.status === "done") {
|
|
5357
|
+
printTaskProgress(completed, tasks.length, task, "skip");
|
|
5358
|
+
continue;
|
|
5359
|
+
}
|
|
5360
|
+
printTaskProgress(completed, tasks.length, task, "run");
|
|
5361
|
+
const taskPrompt = `Task: ${task.id} \u2014 ${task.title}
|
|
5362
|
+
Layer: ${task.layer}
|
|
5363
|
+
Description: ${task.description}
|
|
5364
|
+
Files to touch: ${task.filesToTouch.join(", ") || "as needed"}
|
|
5365
|
+
Acceptance criteria:
|
|
5366
|
+
${task.acceptanceCriteria.map((c) => ` - ${c}`).join("\n")}
|
|
5367
|
+
|
|
5368
|
+
Full spec is at: ${specFilePath}
|
|
5369
|
+
Implement ONLY this task. Do not implement other tasks.`;
|
|
5370
|
+
let taskStatus = "done";
|
|
5371
|
+
try {
|
|
5372
|
+
execSync(`${claudeCmd} -p "${taskPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`, {
|
|
5373
|
+
cwd: workingDir,
|
|
5374
|
+
stdio: "inherit"
|
|
5375
|
+
});
|
|
5376
|
+
completed++;
|
|
5377
|
+
} catch {
|
|
5378
|
+
taskStatus = "failed";
|
|
5379
|
+
console.log(chalk6.yellow(`
|
|
5380
|
+
\u26A0 Task ${task.id} exited with error \u2014 marked as failed. Re-run with --resume to retry.`));
|
|
5381
|
+
}
|
|
5382
|
+
await updateTaskStatus(specFilePath, task.id, taskStatus);
|
|
5383
|
+
}
|
|
5384
|
+
const successCount = tasks.filter((t) => t.status === "done").length + (completed - doneCount);
|
|
5385
|
+
console.log(
|
|
5386
|
+
chalk6.bold(
|
|
5387
|
+
`
|
|
5388
|
+
${successCount === tasks.length ? chalk6.green("\u2714") : chalk6.yellow("!")} Incremental build: ${completed}/${tasks.length} tasks completed.`
|
|
5389
|
+
)
|
|
5390
|
+
);
|
|
5391
|
+
}
|
|
3780
5392
|
// ── Mode: api ─────────────────────────────────────────────────────────────
|
|
3781
|
-
async runApiMode(specFilePath, workingDir, context) {
|
|
5393
|
+
async runApiMode(specFilePath, workingDir, context, options = {}) {
|
|
3782
5394
|
console.log(
|
|
3783
|
-
|
|
5395
|
+
chalk6.blue(
|
|
3784
5396
|
`
|
|
3785
5397
|
\u2500\u2500\u2500 Code Generation: API (${this.provider.providerName}/${this.provider.modelName}) \u2500\u2500\u2500`
|
|
3786
5398
|
)
|
|
3787
5399
|
);
|
|
3788
|
-
const
|
|
5400
|
+
const systemPrompt = getCodeGenSystemPrompt(options.repoType);
|
|
5401
|
+
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
5402
|
+
console.log(chalk6.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
5403
|
+
}
|
|
5404
|
+
const spec = await fs8.readFile(specFilePath, "utf-8");
|
|
3789
5405
|
const constitutionSection = context?.constitution ? `
|
|
3790
5406
|
=== Project Constitution (MUST follow) ===
|
|
3791
5407
|
${context.constitution.slice(0, 2e3)}
|
|
3792
5408
|
` : "";
|
|
3793
5409
|
const contextSummary = context ? `Tech Stack: ${context.techStack.join(", ")}
|
|
3794
5410
|
Existing files: ${context.fileStructure.slice(0, 20).join(", ")}` : "";
|
|
5411
|
+
const installedPackagesSection = buildInstalledPackagesSection(context);
|
|
5412
|
+
const sharedConfigSection = buildSharedConfigSection(context);
|
|
5413
|
+
const dsl = await loadDslForSpec(specFilePath);
|
|
5414
|
+
const dslSection = dsl ? `
|
|
5415
|
+
${buildDslContextSection(dsl)}
|
|
5416
|
+
` : "";
|
|
5417
|
+
if (dsl) {
|
|
5418
|
+
const cmpCount = dsl.components?.length ?? 0;
|
|
5419
|
+
const cmpSuffix = cmpCount > 0 ? `, ${cmpCount} components` : "";
|
|
5420
|
+
console.log(chalk6.green(` \u2713 DSL loaded \u2014 ${dsl.endpoints.length} endpoints, ${dsl.models.length} models${cmpSuffix}`));
|
|
5421
|
+
}
|
|
5422
|
+
const isFrontend = isFrontendDeps(context?.dependencies ?? []);
|
|
5423
|
+
let frontendSection = "";
|
|
5424
|
+
if (isFrontend) {
|
|
5425
|
+
const fctx = await loadFrontendContext(workingDir);
|
|
5426
|
+
frontendSection = `
|
|
5427
|
+
${buildFrontendContextSection(fctx)}
|
|
5428
|
+
`;
|
|
5429
|
+
console.log(chalk6.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
5430
|
+
}
|
|
3795
5431
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
3796
5432
|
if (tasks && tasks.length > 0) {
|
|
3797
|
-
return this.runApiModeWithTasks(spec, tasks, workingDir, constitutionSection);
|
|
5433
|
+
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
3798
5434
|
}
|
|
3799
|
-
console.log(
|
|
5435
|
+
console.log(chalk6.gray(" [1/2] Planning implementation files..."));
|
|
3800
5436
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
3801
5437
|
|
|
5438
|
+
IMPORTANT: Check the "Existing Shared Config Files" section below FIRST. For any file listed there,
|
|
5439
|
+
use action "modify" (never "create") even if you are only adding new entries.
|
|
5440
|
+
IMPORTANT: Check the "Frontend Project Context" section below. Extend existing hooks/services/stores \u2014 do NOT create new parallel utilities.
|
|
5441
|
+
|
|
3802
5442
|
=== Feature Spec ===
|
|
3803
5443
|
${spec}
|
|
3804
|
-
${constitutionSection}
|
|
5444
|
+
${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
|
|
3805
5445
|
=== Project Context ===
|
|
3806
5446
|
${contextSummary}
|
|
3807
5447
|
|
|
@@ -3812,71 +5452,195 @@ Output ONLY a valid JSON array:
|
|
|
3812
5452
|
]`;
|
|
3813
5453
|
let filePlan = [];
|
|
3814
5454
|
try {
|
|
3815
|
-
const planResponse = await this.provider.generate(planPrompt,
|
|
5455
|
+
const planResponse = await this.provider.generate(planPrompt, systemPrompt);
|
|
3816
5456
|
filePlan = parseJsonArray(planResponse);
|
|
3817
5457
|
} catch (err) {
|
|
3818
|
-
console.error(
|
|
5458
|
+
console.error(chalk6.red(" Failed to generate file plan:"), err);
|
|
3819
5459
|
}
|
|
3820
5460
|
if (filePlan.length === 0) {
|
|
3821
|
-
console.log(
|
|
3822
|
-
|
|
5461
|
+
console.log(chalk6.yellow(" Could not determine file plan. Falling back to plan mode."));
|
|
5462
|
+
await this.runPlanMode(specFilePath);
|
|
5463
|
+
return [];
|
|
3823
5464
|
}
|
|
3824
|
-
console.log(
|
|
5465
|
+
console.log(chalk6.cyan(`
|
|
3825
5466
|
Plan: ${filePlan.length} file(s) to process`));
|
|
3826
5467
|
filePlan.forEach((item) => {
|
|
3827
|
-
const icon = item.action === "create" ?
|
|
3828
|
-
console.log(` ${icon} ${item.file}: ${
|
|
5468
|
+
const icon = item.action === "create" ? chalk6.green("+") : chalk6.yellow("~");
|
|
5469
|
+
console.log(` ${icon} ${item.file}: ${chalk6.gray(item.description)}`);
|
|
3829
5470
|
});
|
|
3830
|
-
await this.generateFiles(filePlan, spec, workingDir, constitutionSection);
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
5471
|
+
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
5472
|
+
return files;
|
|
5473
|
+
}
|
|
5474
|
+
async runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection, frontendSection = "", sharedConfigSection = "", options = {}, systemPrompt = getCodeGenSystemPrompt(), context) {
|
|
5475
|
+
const pendingTasks = tasks.filter((t) => t.status !== "done");
|
|
5476
|
+
const doneCount = tasks.length - pendingTasks.length;
|
|
5477
|
+
if (options.resume && doneCount > 0) {
|
|
5478
|
+
console.log(chalk6.cyan(`
|
|
5479
|
+
Task-based generation (resume): ${tasks.length} tasks (${chalk6.green(doneCount + " already done")}, skipping)`));
|
|
5480
|
+
} else if (doneCount > 0) {
|
|
5481
|
+
console.log(chalk6.cyan(`
|
|
5482
|
+
Task-based generation: ${tasks.length} tasks (${chalk6.green(doneCount + " already done")}, resuming from checkpoint)`));
|
|
5483
|
+
} else {
|
|
5484
|
+
console.log(chalk6.cyan(`
|
|
3834
5485
|
Task-based generation: ${tasks.length} tasks`));
|
|
5486
|
+
}
|
|
5487
|
+
const sharedConfigPaths = new Set(
|
|
5488
|
+
(context?.sharedConfigFiles ?? []).map((f) => f.path)
|
|
5489
|
+
);
|
|
5490
|
+
const processedSharedConfigs = /* @__PURE__ */ new Set();
|
|
5491
|
+
const generatedFileCache = /* @__PURE__ */ new Map();
|
|
3835
5492
|
let totalSuccess = 0;
|
|
3836
5493
|
let totalFiles = 0;
|
|
5494
|
+
let completedTasks = doneCount;
|
|
5495
|
+
const allGeneratedFiles = [];
|
|
3837
5496
|
for (const task of tasks) {
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
5497
|
+
if (task.status === "done") {
|
|
5498
|
+
printTaskProgress(completedTasks++, tasks.length, task, "skip");
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
const LAYER_ORDER2 = ["data", "infra", "service", "api", "test"];
|
|
5502
|
+
const layerGroups = [];
|
|
5503
|
+
for (const layer of LAYER_ORDER2) {
|
|
5504
|
+
const group = pendingTasks.filter((t) => t.layer === layer);
|
|
5505
|
+
if (group.length > 0) layerGroups.push({ layer, tasks: group });
|
|
5506
|
+
}
|
|
5507
|
+
const unknownTasks = pendingTasks.filter((t) => !LAYER_ORDER2.includes(t.layer));
|
|
5508
|
+
if (unknownTasks.length > 0) layerGroups.push({ layer: "other", tasks: unknownTasks });
|
|
5509
|
+
for (const { layer, tasks: layerTasks } of layerGroups) {
|
|
5510
|
+
const isParallel = layerTasks.length > 1;
|
|
5511
|
+
const layerIcon = LAYER_ICONS[layer] ?? " ";
|
|
5512
|
+
if (isParallel) {
|
|
5513
|
+
const pct = Math.round(completedTasks / tasks.length * 100);
|
|
5514
|
+
const barWidth = 20;
|
|
5515
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
5516
|
+
const bar = chalk6.green("\u2588".repeat(filled)) + chalk6.gray("\u2591".repeat(barWidth - filled));
|
|
5517
|
+
console.log(
|
|
5518
|
+
chalk6.bold(`
|
|
5519
|
+
[${bar}] ${pct}% \u26A1 Layer [${layer}] ${layerIcon} \u2014 ${layerTasks.length} tasks running in parallel`)
|
|
5520
|
+
);
|
|
5521
|
+
} else {
|
|
5522
|
+
printTaskProgress(completedTasks, tasks.length, layerTasks[0], "run");
|
|
3843
5523
|
}
|
|
3844
|
-
const
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
5524
|
+
const generatedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
5525
|
+
const taskResultPromises = layerTasks.map(async (task) => {
|
|
5526
|
+
if (task.filesToTouch.length === 0) {
|
|
5527
|
+
if (!isParallel) console.log(chalk6.gray(" No files specified, skipping."));
|
|
5528
|
+
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
5529
|
+
}
|
|
5530
|
+
const filePlan = await Promise.all(
|
|
5531
|
+
task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
|
|
5532
|
+
const exists = await fs8.pathExists(path6.join(workingDir, f));
|
|
5533
|
+
return {
|
|
5534
|
+
file: f,
|
|
5535
|
+
action: exists ? "modify" : "create",
|
|
5536
|
+
description: task.description
|
|
5537
|
+
};
|
|
5538
|
+
})
|
|
5539
|
+
);
|
|
5540
|
+
const createsNewFiles = filePlan.some((f) => f.action === "create");
|
|
5541
|
+
const taskText = `${task.title} ${task.description}`.toLowerCase();
|
|
5542
|
+
const impliesRegistration = createsNewFiles && (taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
|
|
5543
|
+
if (filePlan.length === 0) {
|
|
5544
|
+
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration };
|
|
5545
|
+
}
|
|
5546
|
+
const taskContext = `Task: ${task.id} \u2014 ${task.title}
|
|
3850
5547
|
${task.description}
|
|
3851
5548
|
Acceptance: ${task.acceptanceCriteria.join("; ")}`;
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
5549
|
+
const { success, total, files } = await this.generateFiles(
|
|
5550
|
+
filePlan,
|
|
5551
|
+
`${spec}
|
|
3855
5552
|
|
|
3856
5553
|
=== Current Task ===
|
|
3857
5554
|
${taskContext}`,
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
5555
|
+
workingDir,
|
|
5556
|
+
constitutionSection + frontendSection + sharedConfigSection + generatedFilesSection,
|
|
5557
|
+
systemPrompt,
|
|
5558
|
+
isParallel ? task.id : void 0
|
|
5559
|
+
// prefix output lines with task ID in parallel mode
|
|
5560
|
+
);
|
|
5561
|
+
const createdFiles = filePlan.filter((fp) => fp.action === "create").map((fp) => fp.file);
|
|
5562
|
+
return { task, files, createdFiles, success, total, impliesRegistration };
|
|
5563
|
+
});
|
|
5564
|
+
const layerResults = await Promise.all(taskResultPromises);
|
|
5565
|
+
if (isParallel) {
|
|
5566
|
+
console.log("");
|
|
5567
|
+
}
|
|
5568
|
+
for (const result of layerResults) {
|
|
5569
|
+
totalSuccess += result.success;
|
|
5570
|
+
totalFiles += result.total;
|
|
5571
|
+
allGeneratedFiles.push(...result.files);
|
|
5572
|
+
if (isParallel) {
|
|
5573
|
+
const icon = result.success === result.total ? chalk6.green("\u2714") : chalk6.yellow("!");
|
|
5574
|
+
const layerTaskIcon = LAYER_ICONS[result.task.layer] ?? " ";
|
|
5575
|
+
console.log(` ${icon} ${result.task.id} ${layerTaskIcon} ${result.task.title} \u2014 ${result.success}/${result.total} files`);
|
|
5576
|
+
}
|
|
5577
|
+
const taskStatus = result.success === result.total ? "done" : "failed";
|
|
5578
|
+
await updateTaskStatus(specFilePath, result.task.id, taskStatus);
|
|
5579
|
+
if (taskStatus === "failed") {
|
|
5580
|
+
console.log(chalk6.yellow(` \u26A0 ${result.task.id} marked as failed \u2014 re-run with --resume to retry`));
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
completedTasks += layerTasks.length;
|
|
5584
|
+
for (const result of layerResults) {
|
|
5585
|
+
for (const writtenFile of result.files) {
|
|
5586
|
+
if (/src[\\/](api[s]?|services?|stores?|composables?)[\\/]/.test(writtenFile)) {
|
|
5587
|
+
try {
|
|
5588
|
+
const content = await fs8.readFile(path6.join(workingDir, writtenFile), "utf-8");
|
|
5589
|
+
generatedFileCache.set(writtenFile, content);
|
|
5590
|
+
} catch {
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
const anyImpliesRegistration = layerResults.some((r) => r.impliesRegistration);
|
|
5596
|
+
if (anyImpliesRegistration && sharedConfigPaths.size > 0 && context?.sharedConfigFiles) {
|
|
5597
|
+
const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
|
|
5598
|
+
for (const sharedFile of context.sharedConfigFiles) {
|
|
5599
|
+
if (processedSharedConfigs.has(sharedFile.path)) continue;
|
|
5600
|
+
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path6.basename(f).replace(/\.[jt]sx?$/, ""));
|
|
5601
|
+
if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
|
|
5602
|
+
let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
|
|
5603
|
+
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
5604
|
+
purpose = `Add to this file: import ${newModuleNames.join(", ")} from their respective paths and register them in the export/default array. Do NOT remove any existing imports.`;
|
|
5605
|
+
}
|
|
5606
|
+
console.log(chalk6.gray(`
|
|
5607
|
+
+ updating shared config: ${sharedFile.path} [${sharedFile.category}]`));
|
|
5608
|
+
const updatedGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
|
|
5609
|
+
await this.generateFiles(
|
|
5610
|
+
[{ file: sharedFile.path, action: "modify", description: purpose }],
|
|
5611
|
+
`${spec}
|
|
5612
|
+
|
|
5613
|
+
=== Context ===
|
|
5614
|
+
Updating shared registration after layer [${layer}] completed. New modules: ${newModuleNames.join(", ")}.`,
|
|
5615
|
+
workingDir,
|
|
5616
|
+
constitutionSection + frontendSection + sharedConfigSection + updatedGeneratedFilesSection,
|
|
5617
|
+
systemPrompt
|
|
5618
|
+
);
|
|
5619
|
+
processedSharedConfigs.add(sharedFile.path);
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
3863
5622
|
}
|
|
3864
5623
|
console.log(
|
|
3865
|
-
|
|
5624
|
+
chalk6.bold(
|
|
3866
5625
|
`
|
|
3867
|
-
${totalSuccess === totalFiles ?
|
|
5626
|
+
${totalSuccess === totalFiles ? chalk6.green("\u2714") : chalk6.yellow("!")} Task-based generation: ${totalSuccess}/${totalFiles} files written across ${pendingTasks.length} tasks.`
|
|
3868
5627
|
)
|
|
3869
5628
|
);
|
|
5629
|
+
return allGeneratedFiles;
|
|
3870
5630
|
}
|
|
3871
|
-
async generateFiles(filePlan, spec, workingDir, constitutionSection) {
|
|
3872
|
-
|
|
5631
|
+
async generateFiles(filePlan, spec, workingDir, constitutionSection, systemPrompt = getCodeGenSystemPrompt(), taskLabel) {
|
|
5632
|
+
const prefix = taskLabel ? ` [${chalk6.cyan(taskLabel)}] ` : " ";
|
|
5633
|
+
if (!taskLabel) {
|
|
5634
|
+
console.log(chalk6.gray(`
|
|
3873
5635
|
Generating ${filePlan.length} file(s)...`));
|
|
5636
|
+
}
|
|
3874
5637
|
let successCount = 0;
|
|
5638
|
+
const writtenFiles = [];
|
|
3875
5639
|
for (const item of filePlan) {
|
|
3876
|
-
const fullPath =
|
|
5640
|
+
const fullPath = path6.join(workingDir, item.file);
|
|
3877
5641
|
let existingContent = "";
|
|
3878
|
-
if (await
|
|
3879
|
-
existingContent = await
|
|
5642
|
+
if (await fs8.pathExists(fullPath)) {
|
|
5643
|
+
existingContent = await fs8.readFile(fullPath, "utf-8");
|
|
3880
5644
|
}
|
|
3881
5645
|
const codePrompt = `Implement this file.
|
|
3882
5646
|
|
|
@@ -3888,30 +5652,31 @@ ${spec}
|
|
|
3888
5652
|
${constitutionSection}
|
|
3889
5653
|
=== ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
|
|
3890
5654
|
${existingContent || "Output only the complete file content."}`;
|
|
3891
|
-
process.stdout.write(` ${existingContent ? chalk3.yellow("~") : chalk3.green("+")} ${chalk3.bold(item.file)}... `);
|
|
3892
5655
|
try {
|
|
3893
|
-
const raw = await this.provider.generate(codePrompt,
|
|
5656
|
+
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
3894
5657
|
const fileContent = stripCodeFences(raw);
|
|
3895
|
-
await
|
|
3896
|
-
await
|
|
3897
|
-
console.log(
|
|
5658
|
+
await fs8.ensureDir(path6.dirname(fullPath));
|
|
5659
|
+
await fs8.writeFile(fullPath, fileContent, "utf-8");
|
|
5660
|
+
console.log(`${prefix}${existingContent ? chalk6.yellow("~") : chalk6.green("+")} ${chalk6.bold(item.file)} ${chalk6.green("\u2714")}`);
|
|
3898
5661
|
successCount++;
|
|
5662
|
+
writtenFiles.push(item.file);
|
|
3899
5663
|
} catch (err) {
|
|
3900
|
-
console.log(
|
|
3901
|
-
console.error(chalk3.red(` Error: ${err.message}`));
|
|
5664
|
+
console.log(`${prefix}${chalk6.red("\u2718")} ${chalk6.bold(item.file)} \u2014 ${chalk6.red(err.message)}`);
|
|
3902
5665
|
}
|
|
3903
5666
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
5667
|
+
if (!taskLabel) {
|
|
5668
|
+
console.log(
|
|
5669
|
+
chalk6.bold(
|
|
5670
|
+
` ${successCount === filePlan.length ? chalk6.green("\u2714") : chalk6.yellow("!")} ${successCount}/${filePlan.length} files written.`
|
|
5671
|
+
)
|
|
5672
|
+
);
|
|
5673
|
+
}
|
|
5674
|
+
return { success: successCount, total: filePlan.length, files: writtenFiles };
|
|
3910
5675
|
}
|
|
3911
5676
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
3912
5677
|
async runPlanMode(specFilePath) {
|
|
3913
|
-
console.log(
|
|
3914
|
-
const spec = await
|
|
5678
|
+
console.log(chalk6.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5679
|
+
const spec = await fs8.readFile(specFilePath, "utf-8");
|
|
3915
5680
|
const plan = await this.provider.generate(
|
|
3916
5681
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
3917
5682
|
Be specific about:
|
|
@@ -3923,22 +5688,95 @@ Be specific about:
|
|
|
3923
5688
|
${spec}`,
|
|
3924
5689
|
"You are a senior developer creating an actionable implementation guide."
|
|
3925
5690
|
);
|
|
3926
|
-
console.log(
|
|
5691
|
+
console.log(chalk6.cyan("\n") + plan);
|
|
3927
5692
|
}
|
|
3928
5693
|
};
|
|
5694
|
+
var LAYER_ICONS = {
|
|
5695
|
+
data: "\u{1F4BE}",
|
|
5696
|
+
infra: "\u2699\uFE0F ",
|
|
5697
|
+
service: "\u{1F527}",
|
|
5698
|
+
api: "\u{1F310}",
|
|
5699
|
+
test: "\u{1F9EA}"
|
|
5700
|
+
};
|
|
5701
|
+
function printTaskProgress(completed, total, task, mode) {
|
|
5702
|
+
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
5703
|
+
const barWidth = 20;
|
|
5704
|
+
const filled = Math.round(pct / 100 * barWidth);
|
|
5705
|
+
const bar = chalk6.green("\u2588".repeat(filled)) + chalk6.gray("\u2591".repeat(barWidth - filled));
|
|
5706
|
+
const icon = LAYER_ICONS[task.layer] ?? " ";
|
|
5707
|
+
if (mode === "skip") {
|
|
5708
|
+
console.log(
|
|
5709
|
+
chalk6.gray(`
|
|
5710
|
+
[${bar}] ${pct}% \u2713 ${task.id} ${icon} ${task.title} \u2014 already done`)
|
|
5711
|
+
);
|
|
5712
|
+
} else {
|
|
5713
|
+
console.log(
|
|
5714
|
+
chalk6.bold(`
|
|
5715
|
+
[${bar}] ${pct}% \u2192 ${task.id} ${icon} ${task.title}`)
|
|
5716
|
+
);
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
3929
5719
|
|
|
3930
5720
|
// core/reviewer.ts
|
|
3931
|
-
import
|
|
5721
|
+
import chalk7 from "chalk";
|
|
3932
5722
|
import { execSync as execSync2 } from "child_process";
|
|
5723
|
+
import * as path7 from "path";
|
|
5724
|
+
import * as fs9 from "fs-extra";
|
|
5725
|
+
var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5726
|
+
async function loadReviewHistory(projectRoot) {
|
|
5727
|
+
const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
5728
|
+
try {
|
|
5729
|
+
if (await fs9.pathExists(historyPath)) {
|
|
5730
|
+
return await fs9.readJson(historyPath);
|
|
5731
|
+
}
|
|
5732
|
+
} catch {
|
|
5733
|
+
}
|
|
5734
|
+
return [];
|
|
5735
|
+
}
|
|
5736
|
+
async function appendReviewHistory(projectRoot, entry) {
|
|
5737
|
+
const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
5738
|
+
const existing = await loadReviewHistory(projectRoot);
|
|
5739
|
+
const updated = [...existing, entry].slice(-20);
|
|
5740
|
+
try {
|
|
5741
|
+
await fs9.writeJson(historyPath, updated, { spaces: 2 });
|
|
5742
|
+
} catch {
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
function extractScore(reviewText) {
|
|
5746
|
+
const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
5747
|
+
return match ? parseFloat(match[1]) : 0;
|
|
5748
|
+
}
|
|
5749
|
+
function extractTopIssues(reviewText) {
|
|
5750
|
+
const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
|
|
5751
|
+
return issuesSection.split("\n").filter((l) => /^[-·•*]/.test(l.trim())).map((l) => l.replace(/^[-·•*]\s*/, "").trim()).filter(Boolean).slice(0, 3);
|
|
5752
|
+
}
|
|
5753
|
+
function buildHistoryContext(history) {
|
|
5754
|
+
if (history.length === 0) return "";
|
|
5755
|
+
const recent = history.slice(-5);
|
|
5756
|
+
const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
|
|
5757
|
+
for (const entry of recent) {
|
|
5758
|
+
lines.push(`
|
|
5759
|
+
[${entry.date}] ${path7.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
|
|
5760
|
+
entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
|
|
5761
|
+
}
|
|
5762
|
+
return lines.join("\n") + "\n";
|
|
5763
|
+
}
|
|
3933
5764
|
var CodeReviewer = class {
|
|
3934
|
-
constructor(provider) {
|
|
5765
|
+
constructor(provider, projectRoot = process.cwd()) {
|
|
3935
5766
|
this.provider = provider;
|
|
5767
|
+
this.projectRoot = projectRoot;
|
|
3936
5768
|
}
|
|
3937
5769
|
getGitDiff() {
|
|
5770
|
+
const silent = { encoding: "utf-8", stdio: "pipe" };
|
|
5771
|
+
try {
|
|
5772
|
+
execSync2("git rev-parse --is-inside-work-tree", silent);
|
|
5773
|
+
} catch {
|
|
5774
|
+
return "";
|
|
5775
|
+
}
|
|
3938
5776
|
try {
|
|
3939
|
-
let diff = execSync2("git diff --cached",
|
|
3940
|
-
if (!diff.trim()) diff = execSync2("git diff HEAD",
|
|
3941
|
-
if (!diff.trim()) diff = execSync2("git diff",
|
|
5777
|
+
let diff = execSync2("git diff --cached", silent);
|
|
5778
|
+
if (!diff.trim()) diff = execSync2("git diff HEAD", silent);
|
|
5779
|
+
if (!diff.trim()) diff = execSync2("git diff", silent);
|
|
3942
5780
|
return diff;
|
|
3943
5781
|
} catch {
|
|
3944
5782
|
return "";
|
|
@@ -3952,42 +5790,135 @@ var CodeReviewer = class {
|
|
|
3952
5790
|
removed: lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length
|
|
3953
5791
|
};
|
|
3954
5792
|
}
|
|
3955
|
-
|
|
3956
|
-
|
|
5793
|
+
/**
|
|
5794
|
+
* Two-pass review:
|
|
5795
|
+
* Pass 1 — architecture (spec compliance, layer separation, auth)
|
|
5796
|
+
* Pass 2 — implementation details (validation, error handling, edge cases)
|
|
5797
|
+
* + historical issue recurrence check
|
|
5798
|
+
*
|
|
5799
|
+
* Falls back to single-pass if the two-pass flag is not set.
|
|
5800
|
+
*/
|
|
5801
|
+
async runTwoPassReview(specContent, codeContext, specFile) {
|
|
5802
|
+
console.log(chalk7.gray(" Pass 1/2: Architecture review..."));
|
|
5803
|
+
const archPrompt = `Review the architecture of this change.
|
|
5804
|
+
|
|
5805
|
+
=== Feature Spec ===
|
|
5806
|
+
${specContent || "(No spec \u2014 review for general code quality)"}
|
|
5807
|
+
|
|
5808
|
+
=== Code ===
|
|
5809
|
+
${codeContext}`;
|
|
5810
|
+
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
5811
|
+
console.log(chalk7.gray(" Pass 2/2: Implementation review..."));
|
|
5812
|
+
const history = await loadReviewHistory(this.projectRoot);
|
|
5813
|
+
const historyContext = buildHistoryContext(history);
|
|
5814
|
+
const implPrompt = `Review the implementation details of this change.
|
|
5815
|
+
|
|
5816
|
+
=== Feature Spec ===
|
|
5817
|
+
${specContent || "(No spec \u2014 review for general code quality)"}
|
|
5818
|
+
|
|
5819
|
+
=== Code ===
|
|
5820
|
+
${codeContext}
|
|
5821
|
+
|
|
5822
|
+
=== Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
|
|
5823
|
+
${archReview}
|
|
5824
|
+
${historyContext}`;
|
|
5825
|
+
const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
|
|
5826
|
+
const combined = `${archReview}
|
|
5827
|
+
|
|
5828
|
+
${"\u2500".repeat(52)}
|
|
5829
|
+
|
|
5830
|
+
${implReview}`;
|
|
5831
|
+
const score = extractScore(implReview) || extractScore(archReview);
|
|
5832
|
+
const topIssues = extractTopIssues(implReview);
|
|
5833
|
+
if (score > 0 && specFile) {
|
|
5834
|
+
await appendReviewHistory(this.projectRoot, {
|
|
5835
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
5836
|
+
specFile: path7.relative(this.projectRoot, specFile),
|
|
5837
|
+
score,
|
|
5838
|
+
topIssues
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
return combined;
|
|
5842
|
+
}
|
|
5843
|
+
async reviewCode(specContent, specFile) {
|
|
5844
|
+
console.log(chalk7.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3957
5845
|
const diff = this.getGitDiff();
|
|
3958
5846
|
if (!diff.trim()) {
|
|
3959
5847
|
console.log(
|
|
3960
|
-
|
|
5848
|
+
chalk7.yellow(" No git diff found. Stage or commit changes first, then run review.")
|
|
3961
5849
|
);
|
|
3962
|
-
console.log(
|
|
5850
|
+
console.log(chalk7.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
|
|
3963
5851
|
return "No changes";
|
|
3964
5852
|
}
|
|
3965
5853
|
const { files, added, removed } = this.getDiffStats(diff);
|
|
3966
5854
|
console.log(
|
|
3967
|
-
|
|
5855
|
+
chalk7.gray(` Diff: ${files} file(s), ${chalk7.green("+" + added)} ${chalk7.red("-" + removed)}`)
|
|
5856
|
+
);
|
|
5857
|
+
console.log(
|
|
5858
|
+
chalk7.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
3968
5859
|
);
|
|
5860
|
+
const codeContext = diff.slice(0, 1e4);
|
|
5861
|
+
const reviewResult = await this.runTwoPassReview(specContent, codeContext, specFile);
|
|
5862
|
+
console.log(chalk7.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5863
|
+
console.log(reviewResult);
|
|
5864
|
+
console.log(chalk7.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
5865
|
+
return reviewResult;
|
|
5866
|
+
}
|
|
5867
|
+
/**
|
|
5868
|
+
* Review directly from generated file contents (for api mode where git diff is empty).
|
|
5869
|
+
*/
|
|
5870
|
+
async reviewFiles(specContent, filePaths, workingDir, specFile) {
|
|
5871
|
+
console.log(chalk7.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5872
|
+
console.log(chalk7.gray(` Reviewing ${filePaths.length} generated file(s)...`));
|
|
3969
5873
|
console.log(
|
|
3970
|
-
|
|
5874
|
+
chalk7.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
|
|
3971
5875
|
);
|
|
3972
|
-
|
|
5876
|
+
let filesSection = "";
|
|
5877
|
+
for (const filePath of filePaths) {
|
|
5878
|
+
const fullPath = path7.join(workingDir, filePath);
|
|
5879
|
+
try {
|
|
5880
|
+
const content = await fs9.readFile(fullPath, "utf-8");
|
|
5881
|
+
filesSection += `
|
|
3973
5882
|
|
|
3974
|
-
===
|
|
3975
|
-
${
|
|
5883
|
+
=== ${filePath} ===
|
|
5884
|
+
${content.slice(0, 3e3)}`;
|
|
5885
|
+
if (content.length > 3e3) filesSection += `
|
|
5886
|
+
... (truncated, ${content.length} chars total)`;
|
|
5887
|
+
} catch {
|
|
5888
|
+
filesSection += `
|
|
3976
5889
|
|
|
3977
|
-
===
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
5890
|
+
=== ${filePath} ===
|
|
5891
|
+
(file not found)`;
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
const reviewResult = await this.runTwoPassReview(specContent, filesSection, specFile);
|
|
5895
|
+
console.log(chalk7.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3981
5896
|
console.log(reviewResult);
|
|
3982
|
-
console.log(
|
|
5897
|
+
console.log(chalk7.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
3983
5898
|
return reviewResult;
|
|
3984
5899
|
}
|
|
5900
|
+
/** Print score trend from history (last N reviews) */
|
|
5901
|
+
async printScoreTrend(limit = 5) {
|
|
5902
|
+
const history = await loadReviewHistory(this.projectRoot);
|
|
5903
|
+
if (history.length === 0) {
|
|
5904
|
+
console.log(chalk7.gray(" No review history yet."));
|
|
5905
|
+
return;
|
|
5906
|
+
}
|
|
5907
|
+
const recent = history.slice(-limit);
|
|
5908
|
+
console.log(chalk7.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5909
|
+
for (const entry of recent) {
|
|
5910
|
+
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
5911
|
+
const color = entry.score >= 8 ? chalk7.green : entry.score >= 6 ? chalk7.yellow : chalk7.red;
|
|
5912
|
+
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path7.basename(entry.specFile)}`);
|
|
5913
|
+
}
|
|
5914
|
+
console.log(chalk7.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5915
|
+
}
|
|
3985
5916
|
};
|
|
3986
5917
|
|
|
3987
5918
|
// core/constitution-generator.ts
|
|
3988
|
-
import
|
|
3989
|
-
import * as
|
|
3990
|
-
import * as
|
|
5919
|
+
import chalk8 from "chalk";
|
|
5920
|
+
import * as fs10 from "fs-extra";
|
|
5921
|
+
import * as path8 from "path";
|
|
3991
5922
|
|
|
3992
5923
|
// prompts/constitution.prompt.ts
|
|
3993
5924
|
var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
|
|
@@ -4035,9 +5966,24 @@ Output a Markdown document with EXACTLY these sections. Be specific and derive r
|
|
|
4035
5966
|
- \u5FC5\u987B\u8986\u76D6\u7684\u6D4B\u8BD5\u573A\u666F\u7C7B\u578B
|
|
4036
5967
|
- \u6D4B\u8BD5\u6846\u67B6\u548C\u5DE5\u5177
|
|
4037
5968
|
|
|
5969
|
+
## 8. \u5171\u4EAB\u914D\u7F6E\u6587\u4EF6\u6E05\u5355 (Shared Config Files \u2014 Append-Only)
|
|
5970
|
+
|
|
5971
|
+
CRITICAL: The following files are **singleton config files** that already exist in the project.
|
|
5972
|
+
When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
|
|
5973
|
+
appended/merged into these existing files. **NEVER create a new parallel file.**
|
|
5974
|
+
|
|
5975
|
+
For each discovered file, list it as:
|
|
5976
|
+
- \`<relative-path>\` \u2014 <category> \u2014 **MODIFY ONLY, never recreate**
|
|
5977
|
+
|
|
5978
|
+
If the project context includes i18n/locale files: list ALL of them with their paths.
|
|
5979
|
+
If the project context includes constants/enums files: list ALL of them.
|
|
5980
|
+
If the project context includes route index files: list ALL of them.
|
|
5981
|
+
If none are provided in the context, write: "(No shared config files detected \u2014 will be populated on first run)"
|
|
5982
|
+
|
|
4038
5983
|
---
|
|
4039
5984
|
|
|
4040
|
-
Be concise. Each rule must be specific enough to enforce, not a vague principle
|
|
5985
|
+
Be concise. Each rule must be specific enough to enforce, not a vague principle.
|
|
5986
|
+
**Section 8 is the most important section for preventing file duplication bugs.**`;
|
|
4041
5987
|
|
|
4042
5988
|
// core/constitution-generator.ts
|
|
4043
5989
|
var CONSTITUTION_FILE = ".ai-spec-constitution.md";
|
|
@@ -4052,8 +5998,8 @@ var ConstitutionGenerator = class {
|
|
|
4052
5998
|
return this.provider.generate(prompt, constitutionSystemPrompt);
|
|
4053
5999
|
}
|
|
4054
6000
|
async saveConstitution(projectRoot, content) {
|
|
4055
|
-
const filePath =
|
|
4056
|
-
await
|
|
6001
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6002
|
+
await fs10.writeFile(filePath, content, "utf-8");
|
|
4057
6003
|
return filePath;
|
|
4058
6004
|
}
|
|
4059
6005
|
};
|
|
@@ -4085,32 +6031,88 @@ ${context.schema.slice(0, 4e3)}
|
|
|
4085
6031
|
if (context.errorPatterns) {
|
|
4086
6032
|
parts.push(`=== Error Handling Patterns ===
|
|
4087
6033
|
${context.errorPatterns}
|
|
6034
|
+
`);
|
|
6035
|
+
}
|
|
6036
|
+
if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
|
|
6037
|
+
const grouped = context.sharedConfigFiles.reduce(
|
|
6038
|
+
(acc, f) => {
|
|
6039
|
+
(acc[f.category] ??= []).push(f);
|
|
6040
|
+
return acc;
|
|
6041
|
+
},
|
|
6042
|
+
{}
|
|
6043
|
+
);
|
|
6044
|
+
const sections = [];
|
|
6045
|
+
for (const [category, files] of Object.entries(grouped)) {
|
|
6046
|
+
sections.push(`--- ${category} ---`);
|
|
6047
|
+
for (const f of files) {
|
|
6048
|
+
sections.push(`File: ${f.path}
|
|
6049
|
+
${f.preview.slice(0, 600)}
|
|
6050
|
+
`);
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
parts.push(`=== Existing Shared Config Files (Append-Only \u2014 NEVER Recreate) ===
|
|
6054
|
+
${sections.join("\n")}
|
|
4088
6055
|
`);
|
|
4089
6056
|
}
|
|
4090
6057
|
return parts.join("\n");
|
|
4091
6058
|
}
|
|
4092
6059
|
async function loadConstitution(projectRoot) {
|
|
4093
|
-
const filePath =
|
|
4094
|
-
if (await
|
|
4095
|
-
return
|
|
6060
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6061
|
+
if (await fs10.pathExists(filePath)) {
|
|
6062
|
+
return fs10.readFile(filePath, "utf-8");
|
|
4096
6063
|
}
|
|
4097
6064
|
return void 0;
|
|
4098
6065
|
}
|
|
4099
6066
|
function printConstitutionHint(exists) {
|
|
4100
6067
|
if (!exists) {
|
|
4101
6068
|
console.log(
|
|
4102
|
-
|
|
6069
|
+
chalk8.yellow(
|
|
4103
6070
|
" \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
|
|
4104
6071
|
)
|
|
4105
6072
|
);
|
|
4106
6073
|
}
|
|
4107
6074
|
}
|
|
4108
6075
|
|
|
6076
|
+
// core/combined-generator.ts
|
|
6077
|
+
var TASKS_SEPARATOR = "---TASKS_JSON---";
|
|
6078
|
+
var tasksInstruction = `
|
|
6079
|
+
|
|
6080
|
+
---
|
|
6081
|
+
After outputting the complete spec above, append EXACTLY this line on its own (no extra text before or after it):
|
|
6082
|
+
${TASKS_SEPARATOR}
|
|
6083
|
+
Then output a valid JSON array of implementation tasks. Each element must have these exact fields:
|
|
6084
|
+
{"id":"TASK-001","title":"...","description":"1-2 sentences, specific","layer":"data|infra|service|api|test","filesToTouch":["src/..."],"acceptanceCriteria":["verifiable condition"],"dependencies":[],"priority":"high|medium|low"}
|
|
6085
|
+
Layer order: data \u2192 infra \u2192 service \u2192 api \u2192 test. 4-10 tasks total. filesToTouch must use real paths from the project context.`;
|
|
6086
|
+
async function generateSpecWithTasks(provider, idea, context) {
|
|
6087
|
+
const contextBlock = buildTaskPrompt("", context).trim();
|
|
6088
|
+
const fullPrompt = [idea, contextBlock].filter(Boolean).join("\n\n");
|
|
6089
|
+
const combinedSystemPrompt = specPrompt + tasksInstruction;
|
|
6090
|
+
const raw = await provider.generate(fullPrompt, combinedSystemPrompt);
|
|
6091
|
+
return parseSpecAndTasks(raw);
|
|
6092
|
+
}
|
|
6093
|
+
function parseSpecAndTasks(raw) {
|
|
6094
|
+
const sepIdx = raw.indexOf(TASKS_SEPARATOR);
|
|
6095
|
+
if (sepIdx === -1) {
|
|
6096
|
+
return { spec: raw.trim(), tasks: [] };
|
|
6097
|
+
}
|
|
6098
|
+
const spec = raw.slice(0, sepIdx).trim();
|
|
6099
|
+
const tasksRaw = raw.slice(sepIdx + TASKS_SEPARATOR.length).trim();
|
|
6100
|
+
let tasks = [];
|
|
6101
|
+
try {
|
|
6102
|
+
const jsonMatch = tasksRaw.match(/\[[\s\S]*\]/);
|
|
6103
|
+
if (jsonMatch) {
|
|
6104
|
+
tasks = JSON.parse(jsonMatch[0]);
|
|
6105
|
+
}
|
|
6106
|
+
} catch {
|
|
6107
|
+
}
|
|
6108
|
+
return { spec, tasks };
|
|
6109
|
+
}
|
|
6110
|
+
|
|
4109
6111
|
// git/worktree.ts
|
|
4110
6112
|
import { execSync as execSync3 } from "child_process";
|
|
4111
|
-
import * as
|
|
4112
|
-
import * as
|
|
4113
|
-
import
|
|
6113
|
+
import * as path9 from "path";
|
|
6114
|
+
import * as fs11 from "fs-extra";
|
|
6115
|
+
import chalk9 from "chalk";
|
|
4114
6116
|
var GitWorktreeManager = class {
|
|
4115
6117
|
constructor(baseDir) {
|
|
4116
6118
|
this.baseDir = baseDir;
|
|
@@ -4126,38 +6128,73 @@ var GitWorktreeManager = class {
|
|
|
4126
6128
|
sanitizeFeatureName(idea) {
|
|
4127
6129
|
return idea.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 30) || `feature-${Date.now()}`;
|
|
4128
6130
|
}
|
|
6131
|
+
/**
|
|
6132
|
+
* Symlink dependency directories from the base repo into the worktree so that
|
|
6133
|
+
* tools like `vite`, `tsc`, etc. are available without re-installing.
|
|
6134
|
+
*
|
|
6135
|
+
* Handles: node_modules (npm/yarn/pnpm), vendor (PHP Composer)
|
|
6136
|
+
*/
|
|
6137
|
+
async linkDependencies(worktreePath) {
|
|
6138
|
+
const candidates = ["node_modules", "vendor"];
|
|
6139
|
+
for (const dir of candidates) {
|
|
6140
|
+
const src = path9.join(this.baseDir, dir);
|
|
6141
|
+
const dest = path9.join(worktreePath, dir);
|
|
6142
|
+
if (!await fs11.pathExists(src)) continue;
|
|
6143
|
+
if (await fs11.pathExists(dest)) continue;
|
|
6144
|
+
try {
|
|
6145
|
+
await fs11.ensureSymlink(src, dest, "dir");
|
|
6146
|
+
console.log(chalk9.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
6147
|
+
} catch (err) {
|
|
6148
|
+
console.log(chalk9.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
6149
|
+
console.log(chalk9.yellow(` Run \`npm install\` inside the worktree manually.`));
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
4129
6153
|
async createWorktree(idea) {
|
|
4130
6154
|
if (!this.isGitRepo()) {
|
|
4131
|
-
console.log(
|
|
6155
|
+
console.log(chalk9.yellow("\u26A0\uFE0F Not a git repository. Skipping worktree creation."));
|
|
4132
6156
|
return null;
|
|
4133
6157
|
}
|
|
4134
6158
|
const featureName = this.sanitizeFeatureName(idea);
|
|
4135
6159
|
const branchName = `feature/${featureName}`;
|
|
4136
|
-
const repoName =
|
|
4137
|
-
const worktreePath =
|
|
4138
|
-
console.log(
|
|
6160
|
+
const repoName = path9.basename(this.baseDir);
|
|
6161
|
+
const worktreePath = path9.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
6162
|
+
console.log(chalk9.cyan(`
|
|
4139
6163
|
--- Setting up Git Worktree ---`));
|
|
4140
|
-
if (await
|
|
4141
|
-
console.log(
|
|
6164
|
+
if (await fs11.pathExists(worktreePath)) {
|
|
6165
|
+
console.log(chalk9.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
6166
|
+
await this.linkDependencies(worktreePath);
|
|
4142
6167
|
return worktreePath;
|
|
4143
6168
|
}
|
|
4144
6169
|
try {
|
|
4145
6170
|
let branchExists = false;
|
|
4146
6171
|
try {
|
|
4147
|
-
execSync3(`git show-ref --verify refs/heads/${branchName}`, {
|
|
6172
|
+
execSync3(`git show-ref --verify refs/heads/${branchName}`, {
|
|
6173
|
+
cwd: this.baseDir,
|
|
6174
|
+
stdio: "ignore"
|
|
6175
|
+
});
|
|
4148
6176
|
branchExists = true;
|
|
4149
6177
|
} catch {
|
|
4150
6178
|
}
|
|
4151
|
-
console.log(
|
|
6179
|
+
console.log(chalk9.gray(`Creating worktree at: ${worktreePath}`));
|
|
4152
6180
|
if (branchExists) {
|
|
4153
|
-
execSync3(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6181
|
+
execSync3(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
6182
|
+
cwd: this.baseDir,
|
|
6183
|
+
stdio: "inherit"
|
|
6184
|
+
});
|
|
4154
6185
|
} else {
|
|
4155
|
-
execSync3(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
6186
|
+
execSync3(`git worktree add -b ${branchName} "${worktreePath}"`, {
|
|
6187
|
+
cwd: this.baseDir,
|
|
6188
|
+
stdio: "inherit"
|
|
6189
|
+
});
|
|
4156
6190
|
}
|
|
4157
|
-
console.log(
|
|
6191
|
+
console.log(
|
|
6192
|
+
chalk9.green(`\u2714 Worktree successfully created and isolated on branch '${branchName}'`)
|
|
6193
|
+
);
|
|
6194
|
+
await this.linkDependencies(worktreePath);
|
|
4158
6195
|
return worktreePath;
|
|
4159
6196
|
} catch (error) {
|
|
4160
|
-
console.error(
|
|
6197
|
+
console.error(chalk9.red("Failed to create git worktree:"), error);
|
|
4161
6198
|
return null;
|
|
4162
6199
|
}
|
|
4163
6200
|
}
|
|
@@ -4171,18 +6208,25 @@ export {
|
|
|
4171
6208
|
ContextLoader,
|
|
4172
6209
|
DEFAULT_MODELS,
|
|
4173
6210
|
ENV_KEY_MAP,
|
|
6211
|
+
FRONTEND_FRAMEWORKS,
|
|
4174
6212
|
GeminiProvider,
|
|
4175
6213
|
GitWorktreeManager,
|
|
6214
|
+
MiMoProvider,
|
|
4176
6215
|
OpenAICompatibleProvider,
|
|
4177
6216
|
PROVIDER_CATALOG,
|
|
4178
6217
|
SUPPORTED_PROVIDERS,
|
|
4179
6218
|
SpecGenerator,
|
|
4180
6219
|
SpecRefiner,
|
|
4181
6220
|
TaskGenerator,
|
|
6221
|
+
buildTaskPrompt,
|
|
4182
6222
|
createProvider,
|
|
6223
|
+
generateSpecWithTasks,
|
|
6224
|
+
isFrontendDeps,
|
|
4183
6225
|
loadConstitution,
|
|
4184
6226
|
loadTasksForSpec,
|
|
4185
6227
|
printConstitutionHint,
|
|
4186
|
-
|
|
6228
|
+
printTaskProgress,
|
|
6229
|
+
printTasks,
|
|
6230
|
+
updateTaskStatus
|
|
4187
6231
|
};
|
|
4188
6232
|
//# sourceMappingURL=index.mjs.map
|