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