ai-spec-dev 0.30.0 → 0.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-spec-dev",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "AI-driven Development Orchestrator SDK & CLI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/purpose.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 痛点 · 架构创新 · 边界处理 · DSL 的意义 · 当前局限 · 未来方向
4
4
  >
5
- > 当前版本:v0.30.0 · 最后更新:2026-03-29
5
+ > 当前版本:v0.31.0 · 最后更新:2026-03-29
6
6
 
7
7
  ***
8
8
 
@@ -19,7 +19,7 @@
19
19
  7. [当前局限](#7-当前局限)
20
20
  8. [未来优化方向](#8-未来优化方向)
21
21
 
22
- > **版本记录速览**:v0.17.0 宪法截断修复 · v0.18.0 `learn` + `minSpecScore` + 行为契约提取 · v0.19.0 错误解析重写 + Auto Gate 修复 · v0.20.0 `mock --serve` 一键联调 · v0.21.0 store 公开 API 提取修复 · v0.22.0 service/api 层分离 · v0.23.0 view/route 双层 + 文件名幻觉修复 · v0.24.0 四项质量修复(export default、impliesRegistration、依赖拓扑排序、lesson 计数)· v0.25.0 HTTP import 正则、分页提取、isToolCrash 三项修复 · v0.26.0 多仓库 review 目录、batch 容错、tasks JSON 健壮性 · **v0.27.0 可靠性三件套**(Provider retry/timeout/分类、文件快照 + `restore`、RunId 结构化日志)· **v0.28.0 3-pass review**(Pass 3 影响面评估 + 代码复杂度评估)· **v0.29.0 全量审查修复**(RunLogger 完整插桩、update 快照/日志/knowledge、Score Trend 显示影响/复杂度等级、死代码清理)· **v0.30.0 错误修复依赖图排序 + 前端 Import 多行感知解析**
22
+ > **版本记录速览**:v0.17.0 宪法截断修复 · v0.18.0 `learn` + `minSpecScore` + 行为契约提取 · v0.19.0 错误解析重写 + Auto Gate 修复 · v0.20.0 `mock --serve` 一键联调 · v0.21.0 store 公开 API 提取修复 · v0.22.0 service/api 层分离 · v0.23.0 view/route 双层 + 文件名幻觉修复 · v0.24.0 四项质量修复(export default、impliesRegistration、依赖拓扑排序、lesson 计数)· v0.25.0 HTTP import 正则、分页提取、isToolCrash 三项修复 · v0.26.0 多仓库 review 目录、batch 容错、tasks JSON 健壮性 · **v0.27.0 可靠性三件套**(Provider retry/timeout/分类、文件快照 + `restore`、RunId 结构化日志)· **v0.28.0 3-pass review**(Pass 3 影响面评估 + 代码复杂度评估)· **v0.29.0 全量审查修复**(RunLogger 完整插桩、update 快照/日志/knowledge、Score Trend 显示影响/复杂度等级、死代码清理)· **v0.30.0 错误修复依赖图排序 + 前端 Import 多行感知解析** · **v0.31.0 Harness Engineer:Prompt Hash + Create 内联 Self-Eval**
23
23
 
24
24
  ***
25
25
 
@@ -55,10 +55,107 @@ ai-spec 对每个痛点都有对应的架构设计,不是功能堆砌,而是
55
55
 
56
56
  **核心定位**:ai-spec 不是代码补全工具,而是一个「AI 辅助工程流程编排器」。它的目标是让工程师用最少的时间获得一个符合项目规范、通过基本质检、可直接进入 Review 的代码分支。
57
57
 
58
+ ### 1.3 整体架构鸟瞰
59
+
60
+ > 在进入各模块细节之前,先建立一个全局视图。
61
+
62
+ ```mermaid
63
+ flowchart TD
64
+ subgraph INPUT["输入层"]
65
+ IDEA["💬 需求描述\n自然语言"]
66
+ CONST["📜 项目宪法\n§1-§8 规则 + §9 教训"]
67
+ CTX["🗂️ 项目上下文\n代码结构 / 依赖 / 路由"]
68
+ end
69
+
70
+ subgraph CONTRACT["双层契约"]
71
+ SPEC["📄 Spec\nMarkdown 人类可读"]
72
+ DSL["📊 DSL\nJSON 机器可读"]
73
+ end
74
+
75
+ subgraph GATE["质量门控"]
76
+ SCORE_GATE["🎯 Spec 质量评分\nminSpecScore 阈值"]
77
+ APPROVAL["🧑‍💻 Approval Gate\n人工确认后才开始生成"]
78
+ end
79
+
80
+ subgraph GENERATE["生成层"]
81
+ CODEGEN["⚙️ Task 分层代码生成\ndata→service→api→view→route\n层内拓扑排序 + batch 并行"]
82
+ CACHE[("🗄️ File Cache\n行为契约 / 函数签名")]
83
+ end
84
+
85
+ subgraph VERIFY["验证层"]
86
+ ERRLOOP["🔄 错误反馈闭环\n≤2 cycle · 依赖图排序修复"]
87
+ REVIEW["🔬 3-pass 代码审查\n架构 + 实现 + 影响面"]
88
+ end
89
+
90
+ subgraph LEARN["学习层(闭环)"]
91
+ KNOW["📚 §9 知识积累\n审查 issue 自动写入"]
92
+ EVAL["📈 Harness Self-Eval\nharnessScore + promptHash"]
93
+ end
94
+
95
+ IDEA --> SPEC
96
+ CONST -->|"全文注入所有 prompt"| SPEC
97
+ CTX --> SPEC
98
+
99
+ SPEC --> SCORE_GATE
100
+ SCORE_GATE -->|"通过"| APPROVAL
101
+ SCORE_GATE -->|"不足"| STOP1(["🚫 中止"])
102
+ APPROVAL -->|"Proceed"| DSL
103
+ APPROVAL -->|"Abort"| STOP2(["🚫 退出,无残留"])
104
+
105
+ DSL --> CODEGEN
106
+ CODEGEN <-->|"读写"| CACHE
107
+ CODEGEN --> ERRLOOP
108
+ ERRLOOP --> REVIEW
109
+ REVIEW --> KNOW
110
+ KNOW -->|"更新宪法 §9"| CONST
111
+ REVIEW --> EVAL
112
+ ```
113
+
58
114
  ***
59
115
 
60
116
  ## 2. 核心架构设计
61
117
 
118
+ ### 2.0 `ai-spec create` 完整流水线
119
+
120
+ > 以下流程图展示了运行一次 `ai-spec create` 时所有步骤的完整执行路径,包括每个决策门和反馈循环。后续 §2.1—§2.13 各节是对图中各模块的深度解析。
121
+
122
+ ```mermaid
123
+ flowchart TD
124
+ START(["▶ ai-spec create &lt;idea&gt;"])
125
+
126
+ START --> S1["Step 1 · 加载项目上下文\nContextLoader 扫描代码结构 / 依赖 / 路由 / schema"]
127
+ S1 --> S2["Step 2 · Spec + Tasks 生成\n宪法全文注入 prompt 最高优先级"]
128
+ S2 --> S3["Step 3 · 交互式润色\nDiff 预览,可多轮修改"]
129
+ S3 --> S34["Step 3.4 · Spec 质量评估\n覆盖度 / 清晰度 / 宪法符合度打分"]
130
+
131
+ S34 --> G1{Score ≥ minSpecScore?}
132
+ G1 -->|"否"| ABORT1(["🚫 exit(1)\n--force 可强制继续"])
133
+ G1 -->|"是"| G2{"Approval Gate\n展示 Spec + DSL 摘要\n等待人工决策"}
134
+ G2 -->|"Abort"| ABORT2(["🚫 退出\nSpec 不写入磁盘"])
135
+ G2 -->|"Proceed"| DSL["DSL 提取 + 9 条规则校验\n失败最多重试 2 次"]
136
+
137
+ DSL --> TRACK["RunId 生成 + 文件快照初始化\nPrompt Hash 写入 RunLog"]
138
+ TRACK --> CG["Step 5–6 · Task 分层代码生成\ndata → infra → service → api → view → route → test\n层内拓扑排序 → batch 并行 → 缓存更新"]
139
+ CG --> TG["Step 7 · 测试骨架生成"]
140
+
141
+ TG --> EF["Step 8 · 错误反馈闭环"]
142
+ EF --> EF_RUN["运行 test / lint / tsc"]
143
+ EF_RUN --> EF_CHECK{全部通过?}
144
+ EF_CHECK -->|"通过"| RV
145
+ EF_CHECK -->|"有错误 · cycle ≤ 2"| EF_FIX["依赖图排序\nAI 逐文件修复\n携带 DSL 上下文"]
146
+ EF_FIX --> EF_RUN
147
+ EF_CHECK -->|"cycle 2 仍失败"| EF_WARN["⚠️ 黄色警告,继续"]
148
+ EF_WARN --> RV
149
+
150
+ RV["Step 9 · 3-pass 代码审查\nPass1 架构 · Pass2 实现 · Pass3 影响面/复杂度"]
151
+ RV --> KNOW["§9 知识积累\n审查 issue 自动追加宪法"]
152
+
153
+ KNOW --> SE["Step 10 · Harness Self-Eval\nDSL 覆盖 + Compile + Review → harnessScore\nPromptHash 关联,零 AI 调用"]
154
+ SE --> DONE(["✔ Done\nSpec / DSL / 代码 / RunLog 全部落盘"])
155
+ ```
156
+
157
+ ***
158
+
62
159
  ### 2.1 项目宪法:可进化的项目记忆
63
160
 
64
161
  绝大多数 AI 工具的「上下文注入」是静态的——你手动写一段 prompt,每次带进去。ai-spec 的宪法系统不同,它是一个会随项目迭代自动更新的活文档。
@@ -148,6 +245,43 @@ data → infra → service → api → view → route → test
148
245
  | route | — | 路由模块文件(`src/router/routes/`) |
149
246
  | test | 单测、集成测试 | 同左 |
150
247
 
248
+ **执行模型图解:**
249
+
250
+ ```mermaid
251
+ flowchart TB
252
+ subgraph LAYERS["七层顺序(跨层串行)"]
253
+ direction LR
254
+ LA["data"] --> LB["infra"] --> LC["service"] --> LD["api"] --> LE["view"] --> LF["route"] --> LG["test"]
255
+ end
256
+
257
+ subgraph WITHIN["单层内部执行(以 api 层为例)"]
258
+ direction TB
259
+ TOPO["拓扑排序\n按 dependencies 字段分 batch"]
260
+
261
+ subgraph B1["Batch 1 · 无依赖 → 并行"]
262
+ T1["userController.ts"]
263
+ T2["authController.ts"]
264
+ end
265
+
266
+ subgraph B2["Batch 2 · 依赖 Batch 1 → 并行"]
267
+ T3["adminController.ts\n需要 import userController"]
268
+ end
269
+
270
+ TOPO --> B1
271
+ B1 -->|"batch 完成\n更新 FileCache"| B2
272
+ end
273
+
274
+ CACHE[("generatedFileCache\n函数签名 / 文件路径\nbehavioral contracts")]
275
+
276
+ B1 <-->|"读写"| CACHE
277
+ B2 <-->|"读写"| CACHE
278
+
279
+ LAYERS -->|"进入当层"| WITHIN
280
+ WITHIN -->|"层完成\n更新共享 config 文件\n(routes/index.ts 等)"| NEXT(["下一层"])
281
+ ```
282
+
283
+ > **为什么 route 必须在 view 之后?** view 完成后,`TaskManagement.vue` 的真实路径进入 FileCache;route task 生成时 prompt 里已经有 `// exists: src/views/task-management/TaskManagement.vue`,AI 不会再猜测 `index.vue`(v0.23.0 文件名幻觉修复)。
284
+
151
285
  **前端四层链路设计(v0.22.0+)**:`service`(api 调用函数)→ `api`(store,导入 service 函数)→ `view`(页面,使用 store action)→ `route`(路由模块,导入 view 组件)。`route` 必须在 `view` 之后生成,因为路由文件需要知道 view 组件的确切文件名(如 `TaskManagement.vue` 而非猜测的 `index.vue`)——这是 v0.23.0 修复文件名幻觉的核心机制。
152
286
 
153
287
  每个 task 完成后立即写入 `status: done` 到 `tasks.json`,`--resume` 标志让流水线跳过已完成 task,中断恢复精确到 task 粒度。`tasks.json` 文件损坏时(意外截断等)能检测并优雅降级,提示重新生成(v0.26.0)。
@@ -415,6 +549,59 @@ Pass 3 不重复 Pass 1 / Pass 2 的发现,而是站在更高的系统视角
415
549
 
416
550
  ***
417
551
 
552
+ ### 2.13 Harness Engineer:Prompt Hash + Self-Eval(v0.31.0+)
553
+
554
+ #### 背景:缺失的自我评估能力
555
+
556
+ v0.30.0 之前,ai-spec 是一个能自动生成代码的 Harness,但它是一个**没有自我量化能力的 Harness**。你可以感知到某次生成质量好坏,但无法回答:
557
+
558
+ - 修改了 codegen prompt 之后,整体质量是提升还是下降了?
559
+ - 哪个 provider / model 在这个项目上生成质量最稳定?
560
+ - 加严宪法 §9 之后,compile 通过率有没有提高?
561
+
562
+ 这正是 **Harness Engineer** 理念的核心缺口:工程师不只是构建 AI 生成系统,还必须能**量化证明这个系统在变好**。
563
+
564
+ #### Prompt Hash(`core/prompt-hasher.ts`)
565
+
566
+ 每次 `ai-spec create` 启动时,对 6 个核心 prompt 字符串(codegen、DSL extractor、spec generator、review 三 pass)计算 SHA-256 并取前 8 位(如 `a3f2c1d8`),写入 RunLog 根级字段 `promptHash`。
567
+
568
+ ```json
569
+ {
570
+ "runId": "20260329-143022-a7f2",
571
+ "promptHash": "a3f2c1d8",
572
+ "harnessScore": 7.8,
573
+ ...
574
+ }
575
+ ```
576
+
577
+ 任何 prompt 文件的改动都会产生不同的 hash。跨多个 RunLog 对比 `harnessScore` 时,先按 `promptHash` 分组,就能把「prompt 版本差异」从「模型随机性」中解耦,知道分数变化是因为 prompt 改了还是 LLM 本身的波动。
578
+
579
+ #### Create 内联 Self-Eval(`core/self-evaluator.ts`)
580
+
581
+ `ai-spec create` 在 Step 9(code review)之后新增 **Step 10: Harness Self-Eval**,零 AI 调用,纯确定性评分:
582
+
583
+ | 维度 | 评分逻辑 | 权重 |
584
+ |------|---------|------|
585
+ | **DSL Coverage** (0-10) | 检查生成文件是否覆盖了 DSL 声明的 endpoint 层和 model 层 | 40% |
586
+ | **Compile Score** (0-10) | error feedback 全部通过 → 10;未通过 / 跳过 → 5 | 30% |
587
+ | **Review Score** (0-10) | 从 3-pass review 文本提取 `Score: X/10` | 30% |
588
+
589
+ 当 review 被跳过(`--skip-review`)时,权重自动调整为 DSL 55% + Compile 45%。
590
+
591
+ **输出示例:**
592
+
593
+ ```
594
+ ─── Harness Self-Eval ───────────────────────────
595
+ Score : [████████░░] 7.8/10
596
+ DSL : 8/10 Compile: pass Review: 7.2/10
597
+ Prompt : a3f2c1d8
598
+ ─────────────────────────────────────────────────
599
+ ```
600
+
601
+ `harnessScore` 和 `promptHash` 同时写入 RunLog,为日后实现跨运行趋势分析奠定数据基础。
602
+
603
+ ***
604
+
418
605
  ## 3. DSL 层的意义
419
606
 
420
607
  DSL 是整个系统中设计投入最大的模块,也是最容易被误解为「多此一举」的部分。
package/cli/welcome.ts DELETED
@@ -1,151 +0,0 @@
1
- import chalk from "chalk";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import * as fs from "fs-extra";
5
-
6
- // ─── Layout constants ─────────────────────────────────────────────────────────
7
-
8
- const VERSION = "0.14.1";
9
- /** Total visible width of the title / bottom bar */
10
- const TOTAL_W = 76;
11
- /** Visible width of the left content column */
12
- const L_WIDTH = 44;
13
- /** Visible width of the right content column (TOTAL_W - L_WIDTH - 4 for " │ " + leading space) */
14
- const R_WIDTH = TOTAL_W - L_WIDTH - 4;
15
-
16
- // ─── ASCII robot ──────────────────────────────────────────────────────────────
17
-
18
- const ROBOT_COLOR = chalk.hex("#E8885A");
19
-
20
- const ROBOT: string[] = [
21
- ROBOT_COLOR(" ┌───────────┐"),
22
- ROBOT_COLOR(" │") + chalk.bold.white(" ◉ ") + ROBOT_COLOR(" ") + chalk.bold.white("◉ ") + ROBOT_COLOR("│"),
23
- ROBOT_COLOR(" │") + chalk.dim(" ╰─╯ ") + ROBOT_COLOR("│"),
24
- ROBOT_COLOR(" └─────┬─────┘"),
25
- ROBOT_COLOR(" │"),
26
- ROBOT_COLOR(" ────┴────"),
27
- ];
28
-
29
- // ─── Helpers ──────────────────────────────────────────────────────────────────
30
-
31
- /** Visible (non-ANSI) length of a string */
32
- function visLen(s: string): number {
33
- // eslint-disable-next-line no-control-regex
34
- return s.replace(/\x1b\[[0-9;]*m/g, "").length;
35
- }
36
-
37
- /** Pad a string on the right to reach `width` visible chars */
38
- function padR(s: string, width: number): string {
39
- const vl = visLen(s);
40
- return vl >= width ? s : s + " ".repeat(width - vl);
41
- }
42
-
43
- /** Render one full-width row: left column + separator + right column */
44
- function row(left: string, right: string): string {
45
- return padR(left, L_WIDTH) + " " + chalk.gray("│") + " " + padR(right, R_WIDTH);
46
- }
47
-
48
- /** Center a string (with chalk codes) within `width` visible chars */
49
- function center(s: string, width: number): string {
50
- const vl = visLen(s);
51
- const pad = Math.max(0, Math.floor((width - vl) / 2));
52
- return " ".repeat(pad) + s;
53
- }
54
-
55
- // ─── Recent specs ─────────────────────────────────────────────────────────────
56
-
57
- async function getRecentSpecs(dir: string): Promise<string[]> {
58
- const specsDir = path.join(dir, "specs");
59
- if (!(await fs.pathExists(specsDir))) return [];
60
- try {
61
- const files = await fs.readdir(specsDir);
62
- const mdFiles = files.filter((f) => f.endsWith(".md"));
63
- const withStats = await Promise.all(
64
- mdFiles.map(async (f) => {
65
- const stat = await fs.stat(path.join(specsDir, f));
66
- return { name: f, mtime: stat.mtime.getTime() };
67
- })
68
- );
69
- withStats.sort((a, b) => b.mtime - a.mtime);
70
- return withStats.slice(0, 3).map(({ name, mtime }) => {
71
- const ms = Date.now() - mtime;
72
- const hours = Math.floor(ms / 3_600_000);
73
- const days = Math.floor(hours / 24);
74
- const age = days > 0 ? `${days}d ago` : hours > 0 ? `${hours}h ago` : "just now";
75
- // trim to fit right column
76
- const slug = name.replace(/\.md$/, "").slice(0, R_WIDTH - age.length - 2);
77
- return chalk.white(slug) + chalk.dim(" " + age);
78
- });
79
- } catch {
80
- return [];
81
- }
82
- }
83
-
84
- // ─── Public API ───────────────────────────────────────────────────────────────
85
-
86
- export async function printWelcome(
87
- currentDir: string,
88
- config?: { provider?: string; model?: string }
89
- ): Promise<void> {
90
- const username = os.userInfo().username;
91
- const homeDir = os.homedir();
92
- const shortDir = currentDir.startsWith(homeDir)
93
- ? "~" + currentDir.slice(homeDir.length)
94
- : currentDir;
95
-
96
- const recentSpecs = await getRecentSpecs(currentDir);
97
-
98
- const providerBit = config?.provider
99
- ? config.provider + (config.model ? " · " + config.model : "")
100
- : "";
101
- // Build bottom info and truncate to fit left column (leave 2-char indent)
102
- const bottomRaw = [providerBit, shortDir].filter(Boolean).join(" · ");
103
- const maxInfoLen = L_WIDTH - 2;
104
- const bottomTruncated = bottomRaw.length > maxInfoLen
105
- ? bottomRaw.slice(0, maxInfoLen - 1) + "…"
106
- : bottomRaw;
107
- const bottomLine = " " + chalk.dim(bottomTruncated);
108
-
109
- // ── Title bar ───────────────────────────────────────────────────────────────
110
- const titleInner = `ai-spec v${VERSION} `;
111
- const titleDashes = "─".repeat(Math.max(0, TOTAL_W - titleInner.length - 4));
112
- console.log(
113
- "\n" +
114
- chalk.hex("#FF6B35")("─── " + titleInner + titleDashes)
115
- );
116
-
117
- // ── Build left-column lines ──────────────────────────────────────────────────
118
- const welcomeText = "Welcome back, " + chalk.bold.white(username) + "!";
119
- const leftLines: string[] = [
120
- "",
121
- center(welcomeText, L_WIDTH),
122
- "",
123
- ...ROBOT,
124
- "",
125
- bottomLine,
126
- "",
127
- ];
128
-
129
- // ── Build right-column lines ─────────────────────────────────────────────────
130
- const rightLines: string[] = [
131
- chalk.hex("#FF8C00").bold("Tips for getting started"),
132
- chalk.gray("─".repeat(R_WIDTH)),
133
- chalk.white('ai-spec create "feature"'),
134
- chalk.gray("ai-spec workspace run"),
135
- chalk.gray("ai-spec update \"change\""),
136
- "",
137
- chalk.hex("#FF8C00").bold("Recent activity"),
138
- chalk.gray("─".repeat(R_WIDTH)),
139
- ...(recentSpecs.length > 0 ? recentSpecs : [chalk.dim("No recent activity")]),
140
- ];
141
-
142
- // ── Render rows ─────────────────────────────────────────────────────────────
143
- const maxLines = Math.max(leftLines.length, rightLines.length);
144
- for (let i = 0; i < maxLines; i++) {
145
- console.log(row(leftLines[i] ?? "", rightLines[i] ?? ""));
146
- }
147
-
148
- // ── Bottom bar ──────────────────────────────────────────────────────────────
149
- console.log(chalk.gray("─".repeat(TOTAL_W)));
150
- console.log();
151
- }