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 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 if (!opts.auto) {
415
- console.log(chalk.gray(" (Assessment skipped AI call failed or timed out)"));
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
- console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path.basename(entry.specFile)}`);
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, reviewSystemPrompt, reviewArchitectureSystemPrompt, reviewImplementationSystemPrompt, reviewImpactComplexitySystemPrompt;
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
- console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path10.basename(entry.specFile)}`);
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 if (!opts.auto) {
10153
- console.log(import_chalk19.default.gray(" (Assessment skipped \u2014 AI call failed or timed out)"));
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:"));