ai-spec-dev 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/RELEASE_LOG.md +58 -0
- package/cli/index.ts +58 -2
- package/core/reviewer.ts +7 -2
- package/dist/cli/index.js +56 -23
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +56 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +1 -19
- package/purpose.md +26 -14
package/README.md
CHANGED
|
@@ -463,15 +463,34 @@ ai-spec review specs/feature-1234567890.md
|
|
|
463
463
|
ai-spec review --provider glm --model glm-5
|
|
464
464
|
```
|
|
465
465
|
|
|
466
|
-
|
|
466
|
+
**3-pass 输出结构:**
|
|
467
467
|
|
|
468
468
|
```
|
|
469
|
+
─── Pass 1/3: Architecture review ───────────────
|
|
469
470
|
## ✅ 优点 (What's Good)
|
|
470
471
|
## ⚠️ 问题 (Issues Found)
|
|
471
472
|
## 💡 改进建议 (Suggestions)
|
|
472
473
|
## 📊 总体评价 (Overall Assessment) Score: X/10
|
|
474
|
+
|
|
475
|
+
─── Pass 2/3: Implementation review ─────────────
|
|
476
|
+
## ✅ 优点 (What's Good)
|
|
477
|
+
## ⚠️ 问题 (Issues Found)
|
|
478
|
+
## 🔁 历史问题复现 (Recurring Issues)
|
|
479
|
+
## 💡 改进建议 (Suggestions)
|
|
480
|
+
## 📊 综合评分 (Final Score) Score: X/10
|
|
481
|
+
|
|
482
|
+
─── Pass 3/3: Impact & complexity assessment ────
|
|
483
|
+
## 🌊 影响面评估 (Impact Assessment)
|
|
484
|
+
直接影响文件 / 间接范围 / Breaking Changes / 影响等级: 低|中|高
|
|
485
|
+
## 🧮 代码复杂度评估 (Complexity Assessment)
|
|
486
|
+
认知复杂度热点 / 耦合度 / 可维护性风险 / 复杂度等级: 低|中|高
|
|
487
|
+
|
|
488
|
+
─── Review Score Trend ──────────────────────────
|
|
489
|
+
2026-03-26 [████████░░] 8/10 影响:中 复杂度:低 feature-login-v1.md
|
|
473
490
|
```
|
|
474
491
|
|
|
492
|
+
> Pass 3 的影响等级和复杂度等级会持久化到 `.ai-spec-reviews.json`,在历史趋势行中显示,三级颜色编码:高=红、中=黄、低=绿。
|
|
493
|
+
|
|
475
494
|
> 提示:先执行 `git add .` 再运行 `ai-spec review`,确保所有变更都被纳入审查。
|
|
476
495
|
|
|
477
496
|
---
|
|
@@ -600,6 +619,11 @@ ai-spec update "修复描述文字" --skip-affected
|
|
|
600
619
|
| `--provider <name>` | Spec 更新使用的 provider |
|
|
601
620
|
| `--codegen-provider <name>` | 代码生成使用的 provider |
|
|
602
621
|
|
|
622
|
+
> **`--codegen` 附带行为(v0.29.0+)**:
|
|
623
|
+
> - 每次 update 自动生成独立 Run ID,运行摘要(耗时、写入文件数)在结束时打印
|
|
624
|
+
> - 写每个受影响文件前先快照原始内容(`.ai-spec-backup/<runId>/`),可用 `ai-spec restore <runId>` 精确回滚
|
|
625
|
+
> - 完成后自动对更新后的 Spec 运行代码审查,结论写入宪法 §9(与 `create` 的知识积累机制一致)
|
|
626
|
+
|
|
603
627
|
---
|
|
604
628
|
|
|
605
629
|
### `ai-spec export`
|
package/RELEASE_LOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## [0.29.0] 2026-03-27 — 全量审查修复(RunLogger 插桩、update 快照、Score Trend 升级、死代码清理)
|
|
6
|
+
|
|
7
|
+
### 修复内容
|
|
8
|
+
|
|
9
|
+
**Fix #1 — RunLogger 各阶段从未插桩(结构化日志实际为空)**
|
|
10
|
+
- 文件:`cli/index.ts`(create 命令主流水线)
|
|
11
|
+
- 问题:`core/run-logger.ts` 设计了完整的 `stageStart()`/`stageEnd()`/`stageFail()` API,但 CLI 中从未调用,生成的 `.ai-spec-logs/<runId>.json` 的 `entries[]` 数组始终为空——只有开始/结束时间戳和文件列表,丧失了每阶段耗时的核心价值。
|
|
12
|
+
- 修复:在 `create` 流水线所有主要阶段添加 stage 调用,覆盖以下 8 个节点:
|
|
13
|
+
|
|
14
|
+
| Stage Key | 对应步骤 | 记录数据 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `context_load` | Step 1 — 加载项目上下文 | `techStack`, `repoType` |
|
|
17
|
+
| `spec_gen` | Step 2 — Spec + Tasks 生成 | `provider`, `model`, `taskCount`;失败时 `stageFail` |
|
|
18
|
+
| `spec_refine` | Step 3 — 交互式润色 | 耗时 |
|
|
19
|
+
| `spec_assess` | Step 3.4 — Spec 质量评估 | `overallScore`;gate 失败时 `stageFail` |
|
|
20
|
+
| `dsl_extract` | DSL — 结构化提取 | `endpoints`, `models` 数量;提取失败时 `stageFail` |
|
|
21
|
+
| `codegen` | Step 6 — 代码生成 | `mode`, `provider`, `model`, `filesGenerated` |
|
|
22
|
+
| `test_gen` | Step 7 — 测试骨架生成 | `filesGenerated` |
|
|
23
|
+
| `error_feedback` | Step 8 — 错误反馈闭环 | 耗时 |
|
|
24
|
+
| `review` | Step 9 — 3-pass 代码审查 | 耗时 |
|
|
25
|
+
|
|
26
|
+
**Fix #2 — `update --codegen` 写文件前无快照,运行结束无日志**
|
|
27
|
+
- 文件:`cli/index.ts`(update 命令)
|
|
28
|
+
- 问题:`update --codegen` 直接改写受影响文件,但 `setActiveSnapshot()`/`setActiveLogger()` 只在 `create` 命令中调用,`getActiveSnapshot()?.snapshotFile()` 因返回 `null` 而静默跳过,用户无法对 update 产生的改动执行 `ai-spec restore`。
|
|
29
|
+
- 修复:
|
|
30
|
+
- update 命令启动时生成独立 `updateRunId`,初始化 `RunSnapshot` 和 `RunLogger` 并注册为 active 单例
|
|
31
|
+
- 写每个受影响文件前调用 `updateSnapshot.snapshotFile(fullPath)`,现在 `ai-spec restore <updateRunId>` 对 update 生成的改动同样有效
|
|
32
|
+
- 写文件后调用 `updateLogger.fileWritten()`
|
|
33
|
+
- `update_codegen` stage 包含 `stageStart`/`stageEnd`/`stageFail`
|
|
34
|
+
- 命令结束时 `finish()` + `printSummary()` + restore 提示(与 create 对齐)
|
|
35
|
+
|
|
36
|
+
**Fix #3 — `update --codegen` 结束后不积累审查知识**
|
|
37
|
+
- 文件:`cli/index.ts`(update 命令)
|
|
38
|
+
- 问题:`create` 流水线最后会用 `accumulateReviewKnowledge()` 把 review 结论写入宪法 §9;`update --codegen` 虽然也修改了代码,但流程结束后什么都不写。团队在 update 阶段发现的问题从来不进入知识库,宪法无法从迭代修改中学习。
|
|
39
|
+
- 修复:`update --codegen` 完成文件写入后,自动对更新后的 Spec 运行一次 `reviewer.reviewCode()`,并将结果传入 `accumulateReviewKnowledge()`,复用 spec 更新阶段已创建的 `provider` 实例,无需额外配置。
|
|
40
|
+
|
|
41
|
+
**Fix #4 — `reviewSystemPrompt` 旧版单体 prompt 变为死代码未清理**
|
|
42
|
+
- 文件:`prompts/codegen.prompt.ts`、`core/reviewer.ts`
|
|
43
|
+
- 问题:v0.28.0 升级为 3-pass 后,旧的整合式 `reviewSystemPrompt`(18 行)仍在 `codegen.prompt.ts` 中导出,并被 `reviewer.ts` import 但不在任何地方调用,造成维护混淆。
|
|
44
|
+
- 修复:删除 `codegen.prompt.ts` 中的 `reviewSystemPrompt` export,同步删除 `reviewer.ts` 中的对应 import;注释由 `// ─── Two-pass review prompts` 更新为 `// ─── 3-pass review prompts`。
|
|
45
|
+
|
|
46
|
+
### 功能增强
|
|
47
|
+
|
|
48
|
+
**Enhancement — `printScoreTrend()` 新增影响等级 / 复杂度等级展示**
|
|
49
|
+
- 文件:`core/reviewer.ts`
|
|
50
|
+
- 背景:v0.28.0 审查历史 `ReviewHistoryEntry` 已增加 `impactLevel` 和 `complexityLevel` 字段并持久化,但 `printScoreTrend()` 展示趋势时从未读取这两个字段,用户看不到任何影响 / 复杂度信息。
|
|
51
|
+
- 修复:每行趋势输出追加两个彩色标签,颜色编码:高=红、中=黄、低=绿。
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
前(只有分数):
|
|
55
|
+
2026-03-26 [████████░░] 8/10 feature-tasks-v1.md
|
|
56
|
+
|
|
57
|
+
后(新增等级标签):
|
|
58
|
+
2026-03-26 [████████░░] 8/10 影响:中 复杂度:低 feature-tasks-v1.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
5
63
|
## [0.28.0] 2026-03-26 — 三 Pass 代码审查(影响面评估 + 代码复杂度评估)
|
|
6
64
|
|
|
7
65
|
### 新增内容
|
package/cli/index.ts
CHANGED
|
@@ -308,9 +308,11 @@ program
|
|
|
308
308
|
|
|
309
309
|
// ── Step 1: Context ───────────────────────────────────────────────────────
|
|
310
310
|
console.log(chalk.blue("[1/6] Loading project context..."));
|
|
311
|
+
runLogger.stageStart("context_load");
|
|
311
312
|
const loader = new ContextLoader(currentDir);
|
|
312
313
|
const context = await loader.loadProjectContext();
|
|
313
314
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
315
|
+
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
314
316
|
console.log(chalk.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
315
317
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
316
318
|
console.log(chalk.gray(` API files : ${context.apiStructure.length} files`));
|
|
@@ -345,6 +347,7 @@ program
|
|
|
345
347
|
let initialSpec: string;
|
|
346
348
|
let initialTasks: import("../core/task-generator").SpecTask[] = [];
|
|
347
349
|
|
|
350
|
+
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
348
351
|
try {
|
|
349
352
|
if (opts.skipTasks) {
|
|
350
353
|
// Tasks skipped: use SpecGenerator alone
|
|
@@ -363,7 +366,9 @@ program
|
|
|
363
366
|
console.log(chalk.yellow(" ⚠ Tasks not parsed from response — will retry separately after refinement."));
|
|
364
367
|
}
|
|
365
368
|
}
|
|
369
|
+
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
366
370
|
} catch (err) {
|
|
371
|
+
runLogger.stageFail("spec_gen", (err as Error).message);
|
|
367
372
|
console.error(chalk.red(" ✘ Spec generation failed:"), err);
|
|
368
373
|
process.exit(1);
|
|
369
374
|
}
|
|
@@ -375,8 +380,10 @@ program
|
|
|
375
380
|
finalSpec = initialSpec;
|
|
376
381
|
} else {
|
|
377
382
|
console.log(chalk.blue("\n[3/6] Interactive spec refinement..."));
|
|
383
|
+
runLogger.stageStart("spec_refine");
|
|
378
384
|
const refiner = new SpecRefiner(specProvider);
|
|
379
385
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
386
|
+
runLogger.stageEnd("spec_refine");
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
// Compute slug once — used in Approval Gate diff preview and versioned file save
|
|
@@ -393,14 +400,17 @@ program
|
|
|
393
400
|
if (!opts.auto) {
|
|
394
401
|
console.log(chalk.blue("\n[3.4/6] Spec quality assessment..."));
|
|
395
402
|
}
|
|
403
|
+
runLogger.stageStart("spec_assess");
|
|
396
404
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? undefined);
|
|
397
405
|
if (assessment) {
|
|
406
|
+
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
398
407
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
399
408
|
|
|
400
409
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
401
410
|
if (opts.force) {
|
|
402
411
|
console.log(chalk.yellow(`\n ⚠ Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 — bypassed with --force.`));
|
|
403
412
|
} else {
|
|
413
|
+
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
404
414
|
console.log(chalk.red(`\n ✘ Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
405
415
|
if (!opts.auto) {
|
|
406
416
|
console.log(chalk.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
@@ -411,8 +421,11 @@ program
|
|
|
411
421
|
process.exit(1);
|
|
412
422
|
}
|
|
413
423
|
}
|
|
414
|
-
} else
|
|
415
|
-
|
|
424
|
+
} else {
|
|
425
|
+
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
426
|
+
if (!opts.auto) {
|
|
427
|
+
console.log(chalk.gray(" (Assessment skipped — AI call failed or timed out)"));
|
|
428
|
+
}
|
|
416
429
|
}
|
|
417
430
|
}
|
|
418
431
|
|
|
@@ -485,18 +498,22 @@ program
|
|
|
485
498
|
} else {
|
|
486
499
|
console.log(chalk.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
487
500
|
console.log(chalk.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
501
|
+
runLogger.stageStart("dsl_extract");
|
|
488
502
|
try {
|
|
489
503
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
490
504
|
if (isFrontend) console.log(chalk.gray(" Frontend project detected — using ComponentSpec extractor"));
|
|
491
505
|
const dslExtractor = new DslExtractor(specProvider);
|
|
492
506
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
493
507
|
if (extractedDsl) {
|
|
508
|
+
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
494
509
|
console.log(chalk.green(" ✔ DSL extracted and validated."));
|
|
495
510
|
} else {
|
|
511
|
+
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
496
512
|
console.log(chalk.yellow(" ⚠ DSL skipped — codegen will use Spec + Tasks only."));
|
|
497
513
|
}
|
|
498
514
|
} catch (err) {
|
|
499
515
|
// Unexpected error (not user abort — that would have called process.exit)
|
|
516
|
+
runLogger.stageFail("dsl_extract", (err as Error).message);
|
|
500
517
|
console.log(chalk.yellow(` ⚠ DSL extraction error: ${(err as Error).message} — continuing without DSL.`));
|
|
501
518
|
}
|
|
502
519
|
}
|
|
@@ -581,6 +598,7 @@ program
|
|
|
581
598
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
582
599
|
}
|
|
583
600
|
|
|
601
|
+
runLogger.stageStart("codegen", { mode: codegenMode, provider: codegenProviderName, model: codegenModelName });
|
|
584
602
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
585
603
|
const generatedFiles = await codegen.generateCode(specFile, workingDir, context, {
|
|
586
604
|
auto: opts.auto,
|
|
@@ -588,6 +606,7 @@ program
|
|
|
588
606
|
dslFilePath: savedDslFile ?? undefined,
|
|
589
607
|
repoType: detectedRepoType,
|
|
590
608
|
});
|
|
609
|
+
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
591
610
|
|
|
592
611
|
// ── Step 7: Test Skeleton Generation (skipped in TDD mode — tests already written) ──
|
|
593
612
|
if (opts.tdd) {
|
|
@@ -598,8 +617,10 @@ program
|
|
|
598
617
|
console.log(chalk.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
599
618
|
} else {
|
|
600
619
|
console.log(chalk.blue(`\n[7/9] Test skeleton generation...`));
|
|
620
|
+
runLogger.stageStart("test_gen");
|
|
601
621
|
const testGen = new TestGenerator(codegenProvider);
|
|
602
622
|
generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
|
|
623
|
+
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
603
624
|
}
|
|
604
625
|
|
|
605
626
|
// ── Step 8: Error Feedback Loop ───────────────────────────────────────────
|
|
@@ -611,15 +632,18 @@ program
|
|
|
611
632
|
if (opts.tdd) {
|
|
612
633
|
console.log(chalk.cyan("[8/9] TDD mode — error feedback loop driving implementation to pass tests..."));
|
|
613
634
|
}
|
|
635
|
+
runLogger.stageStart("error_feedback");
|
|
614
636
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
615
637
|
maxCycles: opts.tdd ? 3 : 2, // TDD gets one extra cycle
|
|
616
638
|
});
|
|
639
|
+
runLogger.stageEnd("error_feedback");
|
|
617
640
|
}
|
|
618
641
|
|
|
619
642
|
// ── Step 9: Code Review ───────────────────────────────────────────────────
|
|
620
643
|
let reviewResult = "";
|
|
621
644
|
if (!opts.skipReview) {
|
|
622
645
|
console.log(chalk.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
646
|
+
runLogger.stageStart("review");
|
|
623
647
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
624
648
|
const savedSpec = await fs.readFile(specFile, "utf-8");
|
|
625
649
|
|
|
@@ -636,6 +660,7 @@ program
|
|
|
636
660
|
process.chdir(originalDir);
|
|
637
661
|
}
|
|
638
662
|
}
|
|
663
|
+
runLogger.stageEnd("review");
|
|
639
664
|
|
|
640
665
|
// Knowledge Memory: extract lessons from review and append to constitution §9
|
|
641
666
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
@@ -1790,6 +1815,14 @@ program
|
|
|
1790
1815
|
console.log(chalk.blue("\n─── ai-spec update ─────────────────────────────"));
|
|
1791
1816
|
console.log(chalk.gray(` Provider: ${providerName}/${modelName}`));
|
|
1792
1817
|
|
|
1818
|
+
// ── Run tracking (snapshot + log) ─────────────────────────────────────────
|
|
1819
|
+
const updateRunId = generateRunId();
|
|
1820
|
+
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
1821
|
+
setActiveSnapshot(updateSnapshot);
|
|
1822
|
+
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
1823
|
+
setActiveLogger(updateLogger);
|
|
1824
|
+
console.log(chalk.gray(` Run ID: ${updateRunId}`));
|
|
1825
|
+
|
|
1793
1826
|
// ── Find existing spec ────────────────────────────────────────────────────
|
|
1794
1827
|
let specPath: string | null = opts.spec ?? null;
|
|
1795
1828
|
if (!specPath) {
|
|
@@ -1860,6 +1893,7 @@ program
|
|
|
1860
1893
|
? `\n=== DSL Context ===\n${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3000)}\n`
|
|
1861
1894
|
: "";
|
|
1862
1895
|
|
|
1896
|
+
updateLogger.stageStart("update_codegen");
|
|
1863
1897
|
for (const affected of result.affectedFiles) {
|
|
1864
1898
|
const fullPath = path.join(currentDir, affected.file);
|
|
1865
1899
|
let existing = "";
|
|
@@ -1883,12 +1917,34 @@ ${existing || "Create from scratch."}`;
|
|
|
1883
1917
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
1884
1918
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
1885
1919
|
await fs.ensureDir(path.dirname(fullPath));
|
|
1920
|
+
// Snapshot original before overwrite so `ai-spec restore` can undo
|
|
1921
|
+
await updateSnapshot.snapshotFile(fullPath);
|
|
1886
1922
|
await fs.writeFile(fullPath, content, "utf-8");
|
|
1923
|
+
updateLogger.fileWritten(affected.file);
|
|
1887
1924
|
console.log(chalk.green("✔"));
|
|
1888
1925
|
} catch (err) {
|
|
1926
|
+
updateLogger.stageFail("update_codegen", `${affected.file}: ${(err as Error).message}`);
|
|
1889
1927
|
console.log(chalk.red(`✘ ${(err as Error).message}`));
|
|
1890
1928
|
}
|
|
1891
1929
|
}
|
|
1930
|
+
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
1931
|
+
|
|
1932
|
+
// Knowledge Memory: run a lightweight review on the updated spec and accumulate lessons
|
|
1933
|
+
const updatedSpecContent = await fs.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
1934
|
+
if (updatedSpecContent) {
|
|
1935
|
+
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
1936
|
+
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
1937
|
+
if (reviewResult && reviewResult !== "No changes") {
|
|
1938
|
+
await accumulateReviewKnowledge(provider, currentDir, reviewResult);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// ── Finish run tracking ────────────────────────────────────────────────────
|
|
1944
|
+
updateLogger.finish();
|
|
1945
|
+
updateLogger.printSummary();
|
|
1946
|
+
if (updateSnapshot.fileCount > 0) {
|
|
1947
|
+
console.log(chalk.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
1892
1948
|
}
|
|
1893
1949
|
|
|
1894
1950
|
// ── Hints ─────────────────────────────────────────────────────────────────
|
package/core/reviewer.ts
CHANGED
|
@@ -4,7 +4,6 @@ import * as path from "path";
|
|
|
4
4
|
import * as fs from "fs-extra";
|
|
5
5
|
import { AIProvider } from "./spec-generator";
|
|
6
6
|
import {
|
|
7
|
-
reviewSystemPrompt,
|
|
8
7
|
reviewArchitectureSystemPrompt,
|
|
9
8
|
reviewImplementationSystemPrompt,
|
|
10
9
|
reviewImpactComplexitySystemPrompt,
|
|
@@ -289,7 +288,13 @@ ${implReview}`;
|
|
|
289
288
|
for (const entry of recent) {
|
|
290
289
|
const bar = "█".repeat(entry.score) + "░".repeat(10 - entry.score);
|
|
291
290
|
const color = entry.score >= 8 ? chalk.green : entry.score >= 6 ? chalk.yellow : chalk.red;
|
|
292
|
-
|
|
291
|
+
const impactTag = entry.impactLevel
|
|
292
|
+
? chalk.gray(` 影响:${entry.impactLevel === "高" ? chalk.red(entry.impactLevel) : entry.impactLevel === "中" ? chalk.yellow(entry.impactLevel) : chalk.green(entry.impactLevel)}`)
|
|
293
|
+
: "";
|
|
294
|
+
const complexityTag = entry.complexityLevel
|
|
295
|
+
? chalk.gray(` 复杂度:${entry.complexityLevel === "高" ? chalk.red(entry.complexityLevel) : entry.complexityLevel === "中" ? chalk.yellow(entry.complexityLevel) : chalk.green(entry.complexityLevel)}`)
|
|
296
|
+
: "";
|
|
297
|
+
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path.basename(entry.specFile)}`);
|
|
293
298
|
}
|
|
294
299
|
console.log(chalk.cyan("─────────────────────────────────────────────────"));
|
|
295
300
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -42,8 +42,7 @@ __export(codegen_prompt_exports, {
|
|
|
42
42
|
getCodeGenSystemPrompt: () => getCodeGenSystemPrompt,
|
|
43
43
|
reviewArchitectureSystemPrompt: () => reviewArchitectureSystemPrompt,
|
|
44
44
|
reviewImpactComplexitySystemPrompt: () => reviewImpactComplexitySystemPrompt,
|
|
45
|
-
reviewImplementationSystemPrompt: () => reviewImplementationSystemPrompt
|
|
46
|
-
reviewSystemPrompt: () => reviewSystemPrompt
|
|
45
|
+
reviewImplementationSystemPrompt: () => reviewImplementationSystemPrompt
|
|
47
46
|
});
|
|
48
47
|
function getCodeGenSystemPrompt(repoType) {
|
|
49
48
|
switch (repoType) {
|
|
@@ -61,7 +60,7 @@ function getCodeGenSystemPrompt(repoType) {
|
|
|
61
60
|
return codeGenSystemPrompt;
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
|
-
var codeGenSystemPrompt, codeGenGoSystemPrompt, codeGenPythonSystemPrompt, codeGenJavaSystemPrompt, codeGenRustSystemPrompt, codeGenPhpSystemPrompt,
|
|
63
|
+
var codeGenSystemPrompt, codeGenGoSystemPrompt, codeGenPythonSystemPrompt, codeGenJavaSystemPrompt, codeGenRustSystemPrompt, codeGenPhpSystemPrompt, reviewArchitectureSystemPrompt, reviewImplementationSystemPrompt, reviewImpactComplexitySystemPrompt;
|
|
65
64
|
var init_codegen_prompt = __esm({
|
|
66
65
|
"prompts/codegen.prompt.ts"() {
|
|
67
66
|
"use strict";
|
|
@@ -211,23 +210,6 @@ CRITICAL \u2014 File Reuse Rules:
|
|
|
211
210
|
10. Register new routes in the EXISTING routes/api.php (or routes/web.php) \u2014 never create a parallel routes file.
|
|
212
211
|
11. Add new Eloquent model methods to the EXISTING Model class \u2014 never create a parallel model file.
|
|
213
212
|
12. Add new service methods to the EXISTING service class if one already covers the same resource.`;
|
|
214
|
-
reviewSystemPrompt = `You are a Senior Software Architect conducting a thorough code review. Your review should be structured, constructive, and specific.
|
|
215
|
-
|
|
216
|
-
Always format your review with these exact sections:
|
|
217
|
-
|
|
218
|
-
## \u2705 \u4F18\u70B9 (What's Good)
|
|
219
|
-
List specific things done well.
|
|
220
|
-
|
|
221
|
-
## \u26A0\uFE0F \u95EE\u9898 (Issues Found)
|
|
222
|
-
List bugs, security issues, or spec deviations with line references.
|
|
223
|
-
|
|
224
|
-
## \u{1F4A1} \u6539\u8FDB\u5EFA\u8BAE (Suggestions)
|
|
225
|
-
Actionable improvements that would elevate code quality.
|
|
226
|
-
|
|
227
|
-
## \u{1F4CA} \u603B\u4F53\u8BC4\u4EF7 (Overall Assessment)
|
|
228
|
-
Score: X/10 \u2014 One-paragraph summary.
|
|
229
|
-
|
|
230
|
-
Be specific. Reference actual code, not vague principles.`;
|
|
231
213
|
reviewArchitectureSystemPrompt = `You are a Senior Software Architect reviewing the HIGH-LEVEL design of a code change.
|
|
232
214
|
|
|
233
215
|
Focus ONLY on:
|
|
@@ -7047,7 +7029,9 @@ ${content.slice(0, 3e3)}`;
|
|
|
7047
7029
|
for (const entry of recent) {
|
|
7048
7030
|
const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
|
|
7049
7031
|
const color = entry.score >= 8 ? import_chalk9.default.green : entry.score >= 6 ? import_chalk9.default.yellow : import_chalk9.default.red;
|
|
7050
|
-
|
|
7032
|
+
const impactTag = entry.impactLevel ? import_chalk9.default.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? import_chalk9.default.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? import_chalk9.default.yellow(entry.impactLevel) : import_chalk9.default.green(entry.impactLevel)}`) : "";
|
|
7033
|
+
const complexityTag = entry.complexityLevel ? import_chalk9.default.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? import_chalk9.default.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? import_chalk9.default.yellow(entry.complexityLevel) : import_chalk9.default.green(entry.complexityLevel)}`) : "";
|
|
7034
|
+
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path10.basename(entry.specFile)}`);
|
|
7051
7035
|
}
|
|
7052
7036
|
console.log(import_chalk9.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"));
|
|
7053
7037
|
}
|
|
@@ -10061,9 +10045,11 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10061
10045
|
});
|
|
10062
10046
|
setActiveLogger(runLogger);
|
|
10063
10047
|
console.log(import_chalk19.default.blue("[1/6] Loading project context..."));
|
|
10048
|
+
runLogger.stageStart("context_load");
|
|
10064
10049
|
const loader = new ContextLoader(currentDir);
|
|
10065
10050
|
const context = await loader.loadProjectContext();
|
|
10066
10051
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
10052
|
+
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
10067
10053
|
console.log(import_chalk19.default.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
10068
10054
|
console.log(import_chalk19.default.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
10069
10055
|
console.log(import_chalk19.default.gray(` API files : ${context.apiStructure.length} files`));
|
|
@@ -10094,6 +10080,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10094
10080
|
const specProvider = createProvider(specProviderName, specApiKey, specModelName);
|
|
10095
10081
|
let initialSpec;
|
|
10096
10082
|
let initialTasks = [];
|
|
10083
|
+
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
10097
10084
|
try {
|
|
10098
10085
|
if (opts.skipTasks) {
|
|
10099
10086
|
const generator = new SpecGenerator(specProvider);
|
|
@@ -10110,7 +10097,9 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10110
10097
|
console.log(import_chalk19.default.yellow(" \u26A0 Tasks not parsed from response \u2014 will retry separately after refinement."));
|
|
10111
10098
|
}
|
|
10112
10099
|
}
|
|
10100
|
+
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
10113
10101
|
} catch (err) {
|
|
10102
|
+
runLogger.stageFail("spec_gen", err.message);
|
|
10114
10103
|
console.error(import_chalk19.default.red(" \u2718 Spec generation failed:"), err);
|
|
10115
10104
|
process.exit(1);
|
|
10116
10105
|
}
|
|
@@ -10120,8 +10109,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10120
10109
|
finalSpec = initialSpec;
|
|
10121
10110
|
} else {
|
|
10122
10111
|
console.log(import_chalk19.default.blue("\n[3/6] Interactive spec refinement..."));
|
|
10112
|
+
runLogger.stageStart("spec_refine");
|
|
10123
10113
|
const refiner = new SpecRefiner(specProvider);
|
|
10124
10114
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
10115
|
+
runLogger.stageEnd("spec_refine");
|
|
10125
10116
|
}
|
|
10126
10117
|
const featureSlug = slugify(idea);
|
|
10127
10118
|
const minScore = config2.minSpecScore ?? 0;
|
|
@@ -10130,14 +10121,17 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10130
10121
|
if (!opts.auto) {
|
|
10131
10122
|
console.log(import_chalk19.default.blue("\n[3.4/6] Spec quality assessment..."));
|
|
10132
10123
|
}
|
|
10124
|
+
runLogger.stageStart("spec_assess");
|
|
10133
10125
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? void 0);
|
|
10134
10126
|
if (assessment) {
|
|
10127
|
+
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
10135
10128
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
10136
10129
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
10137
10130
|
if (opts.force) {
|
|
10138
10131
|
console.log(import_chalk19.default.yellow(`
|
|
10139
10132
|
\u26A0 Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 \u2014 bypassed with --force.`));
|
|
10140
10133
|
} else {
|
|
10134
|
+
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
10141
10135
|
console.log(import_chalk19.default.red(`
|
|
10142
10136
|
\u2718 Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
10143
10137
|
if (!opts.auto) {
|
|
@@ -10149,8 +10143,11 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10149
10143
|
process.exit(1);
|
|
10150
10144
|
}
|
|
10151
10145
|
}
|
|
10152
|
-
} else
|
|
10153
|
-
|
|
10146
|
+
} else {
|
|
10147
|
+
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
10148
|
+
if (!opts.auto) {
|
|
10149
|
+
console.log(import_chalk19.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
|
|
10150
|
+
}
|
|
10154
10151
|
}
|
|
10155
10152
|
}
|
|
10156
10153
|
if (!opts.auto) {
|
|
@@ -10208,17 +10205,21 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10208
10205
|
} else {
|
|
10209
10206
|
console.log(import_chalk19.default.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
10210
10207
|
console.log(import_chalk19.default.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
10208
|
+
runLogger.stageStart("dsl_extract");
|
|
10211
10209
|
try {
|
|
10212
10210
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
10213
10211
|
if (isFrontend) console.log(import_chalk19.default.gray(" Frontend project detected \u2014 using ComponentSpec extractor"));
|
|
10214
10212
|
const dslExtractor = new DslExtractor(specProvider);
|
|
10215
10213
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
10216
10214
|
if (extractedDsl) {
|
|
10215
|
+
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
10217
10216
|
console.log(import_chalk19.default.green(" \u2714 DSL extracted and validated."));
|
|
10218
10217
|
} else {
|
|
10218
|
+
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
10219
10219
|
console.log(import_chalk19.default.yellow(" \u26A0 DSL skipped \u2014 codegen will use Spec + Tasks only."));
|
|
10220
10220
|
}
|
|
10221
10221
|
} catch (err) {
|
|
10222
|
+
runLogger.stageFail("dsl_extract", err.message);
|
|
10222
10223
|
console.log(import_chalk19.default.yellow(` \u26A0 DSL extraction error: ${err.message} \u2014 continuing without DSL.`));
|
|
10223
10224
|
}
|
|
10224
10225
|
}
|
|
@@ -10276,6 +10277,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10276
10277
|
const testGen = new TestGenerator(codegenProvider);
|
|
10277
10278
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
10278
10279
|
}
|
|
10280
|
+
runLogger.stageStart("codegen", { mode: codegenMode, provider: codegenProviderName, model: codegenModelName });
|
|
10279
10281
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
10280
10282
|
const generatedFiles = await codegen.generateCode(specFile, workingDir, context, {
|
|
10281
10283
|
auto: opts.auto,
|
|
@@ -10283,6 +10285,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10283
10285
|
dslFilePath: savedDslFile ?? void 0,
|
|
10284
10286
|
repoType: detectedRepoType
|
|
10285
10287
|
});
|
|
10288
|
+
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
10286
10289
|
if (opts.tdd) {
|
|
10287
10290
|
console.log(import_chalk19.default.gray("\n[7/9] TDD mode \u2014 test files already written pre-implementation."));
|
|
10288
10291
|
} else if (opts.skipTests) {
|
|
@@ -10292,8 +10295,10 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10292
10295
|
} else {
|
|
10293
10296
|
console.log(import_chalk19.default.blue(`
|
|
10294
10297
|
[7/9] Test skeleton generation...`));
|
|
10298
|
+
runLogger.stageStart("test_gen");
|
|
10295
10299
|
const testGen = new TestGenerator(codegenProvider);
|
|
10296
10300
|
generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
|
|
10301
|
+
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
10297
10302
|
}
|
|
10298
10303
|
if (opts.skipErrorFeedback) {
|
|
10299
10304
|
console.log(import_chalk19.default.gray("[8/9] Skipping error feedback (--skip-error-feedback)."));
|
|
@@ -10301,14 +10306,17 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10301
10306
|
if (opts.tdd) {
|
|
10302
10307
|
console.log(import_chalk19.default.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10303
10308
|
}
|
|
10309
|
+
runLogger.stageStart("error_feedback");
|
|
10304
10310
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
10305
10311
|
maxCycles: opts.tdd ? 3 : 2
|
|
10306
10312
|
// TDD gets one extra cycle
|
|
10307
10313
|
});
|
|
10314
|
+
runLogger.stageEnd("error_feedback");
|
|
10308
10315
|
}
|
|
10309
10316
|
let reviewResult = "";
|
|
10310
10317
|
if (!opts.skipReview) {
|
|
10311
10318
|
console.log(import_chalk19.default.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
10319
|
+
runLogger.stageStart("review");
|
|
10312
10320
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
10313
10321
|
const savedSpec = await fs24.readFile(specFile, "utf-8");
|
|
10314
10322
|
if (codegenMode === "api" && generatedFiles.length > 0) {
|
|
@@ -10322,6 +10330,7 @@ program.command("create").description("Generate a feature spec and kick off code
|
|
|
10322
10330
|
process.chdir(originalDir);
|
|
10323
10331
|
}
|
|
10324
10332
|
}
|
|
10333
|
+
runLogger.stageEnd("review");
|
|
10325
10334
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
10326
10335
|
}
|
|
10327
10336
|
runLogger.finish();
|
|
@@ -11170,6 +11179,12 @@ program.command("update").description("Update an existing spec with a change req
|
|
|
11170
11179
|
const provider = createProvider(providerName, apiKey, modelName);
|
|
11171
11180
|
console.log(import_chalk19.default.blue("\n\u2500\u2500\u2500 ai-spec update \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"));
|
|
11172
11181
|
console.log(import_chalk19.default.gray(` Provider: ${providerName}/${modelName}`));
|
|
11182
|
+
const updateRunId = generateRunId();
|
|
11183
|
+
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
11184
|
+
setActiveSnapshot(updateSnapshot);
|
|
11185
|
+
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
11186
|
+
setActiveLogger(updateLogger);
|
|
11187
|
+
console.log(import_chalk19.default.gray(` Run ID: ${updateRunId}`));
|
|
11173
11188
|
let specPath = opts.spec ?? null;
|
|
11174
11189
|
if (!specPath) {
|
|
11175
11190
|
const specsDir = path23.join(currentDir, "specs");
|
|
@@ -11228,6 +11243,7 @@ ${context.constitution}
|
|
|
11228
11243
|
=== DSL Context ===
|
|
11229
11244
|
${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3e3)}
|
|
11230
11245
|
` : "";
|
|
11246
|
+
updateLogger.stageStart("update_codegen");
|
|
11231
11247
|
for (const affected of result.affectedFiles) {
|
|
11232
11248
|
const fullPath = path23.join(currentDir, affected.file);
|
|
11233
11249
|
let existing = "";
|
|
@@ -11252,12 +11268,29 @@ ${existing || "Create from scratch."}`;
|
|
|
11252
11268
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
11253
11269
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
11254
11270
|
await fs24.ensureDir(path23.dirname(fullPath));
|
|
11271
|
+
await updateSnapshot.snapshotFile(fullPath);
|
|
11255
11272
|
await fs24.writeFile(fullPath, content, "utf-8");
|
|
11273
|
+
updateLogger.fileWritten(affected.file);
|
|
11256
11274
|
console.log(import_chalk19.default.green("\u2714"));
|
|
11257
11275
|
} catch (err) {
|
|
11276
|
+
updateLogger.stageFail("update_codegen", `${affected.file}: ${err.message}`);
|
|
11258
11277
|
console.log(import_chalk19.default.red(`\u2718 ${err.message}`));
|
|
11259
11278
|
}
|
|
11260
11279
|
}
|
|
11280
|
+
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
11281
|
+
const updatedSpecContent = await fs24.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
11282
|
+
if (updatedSpecContent) {
|
|
11283
|
+
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
11284
|
+
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
11285
|
+
if (reviewResult && reviewResult !== "No changes") {
|
|
11286
|
+
await accumulateReviewKnowledge(provider, currentDir, reviewResult);
|
|
11287
|
+
}
|
|
11288
|
+
}
|
|
11289
|
+
}
|
|
11290
|
+
updateLogger.finish();
|
|
11291
|
+
updateLogger.printSummary();
|
|
11292
|
+
if (updateSnapshot.fileCount > 0) {
|
|
11293
|
+
console.log(import_chalk19.default.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
11261
11294
|
}
|
|
11262
11295
|
if (!opts.codegen && result.affectedFiles.length > 0) {
|
|
11263
11296
|
console.log(import_chalk19.default.blue("\n Next steps:"));
|