ai-spec-dev 0.30.1 → 0.33.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 +5 -1
- package/README.md +29 -1
- package/RELEASE_LOG.md +188 -0
- package/cli/commands/config.ts +93 -0
- package/cli/commands/export.ts +66 -0
- package/cli/commands/init.ts +153 -0
- package/cli/commands/learn.ts +30 -0
- package/cli/commands/logs.ts +106 -0
- package/cli/commands/model.ts +156 -0
- package/cli/commands/restore.ts +22 -0
- package/cli/commands/review.ts +63 -0
- package/cli/commands/trend.ts +36 -0
- package/cli/commands/update.ts +178 -0
- package/cli/commands/workspace.ts +219 -0
- package/cli/index.ts +301 -1
- package/cli/utils.ts +83 -0
- package/core/dsl-feedback.ts +255 -0
- package/core/prompt-hasher.ts +42 -0
- package/core/run-logger.ts +21 -0
- package/core/run-trend.ts +241 -0
- package/core/self-evaluator.ts +276 -0
- package/dist/cli/index.js +1089 -445
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1089 -445
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
- package/purpose.md +189 -2
- package/tests/dsl-extractor.test.ts +264 -0
- package/tests/dsl-feedback.test.ts +266 -0
- package/tests/dsl-validator.test.ts +283 -0
- package/tests/error-feedback.test.ts +292 -0
- package/tests/provider-utils.test.ts +173 -0
- package/tests/run-trend.test.ts +186 -0
- package/tests/self-evaluator.test.ts +339 -0
- package/tests/spec-assessor.test.ts +142 -0
- package/tests/task-generator.test.ts +230 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-spec-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "AI-driven Development Orchestrator SDK & CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsup",
|
|
12
|
-
"dev": "tsup --watch"
|
|
12
|
+
"dev": "tsup --watch",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest"
|
|
13
15
|
},
|
|
14
16
|
"keywords": [
|
|
15
17
|
"ai",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"glob": "^13.0.6",
|
|
42
44
|
"ts-node": "^10.9.2",
|
|
43
45
|
"tsup": "^8.4.0",
|
|
44
|
-
"typescript": "^5.7.3"
|
|
46
|
+
"typescript": "^5.7.3",
|
|
47
|
+
"vitest": "^2.1.0"
|
|
45
48
|
}
|
|
46
49
|
}
|
package/purpose.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 痛点 · 架构创新 · 边界处理 · DSL 的意义 · 当前局限 · 未来方向
|
|
4
4
|
>
|
|
5
|
-
> 当前版本:v0.
|
|
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 <idea>"])
|
|
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 是整个系统中设计投入最大的模块,也是最容易被误解为「多此一举」的部分。
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
dslFilePath,
|
|
4
|
+
buildDslContextSection,
|
|
5
|
+
DslExtractor,
|
|
6
|
+
} from "../core/dsl-extractor";
|
|
7
|
+
import type { SpecDSL } from "../core/dsl-types";
|
|
8
|
+
import type { AIProvider } from "../core/spec-generator";
|
|
9
|
+
|
|
10
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const VALID_DSL: SpecDSL = {
|
|
13
|
+
version: "1.0",
|
|
14
|
+
feature: {
|
|
15
|
+
id: "user-login",
|
|
16
|
+
title: "User Login",
|
|
17
|
+
description: "Authenticate users with email and password",
|
|
18
|
+
},
|
|
19
|
+
models: [
|
|
20
|
+
{
|
|
21
|
+
name: "User",
|
|
22
|
+
fields: [
|
|
23
|
+
{ name: "id", type: "String", required: true },
|
|
24
|
+
{ name: "email", type: "String", required: true, unique: true },
|
|
25
|
+
],
|
|
26
|
+
relations: ["has many Session"],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
endpoints: [
|
|
30
|
+
{
|
|
31
|
+
id: "EP-001",
|
|
32
|
+
method: "POST",
|
|
33
|
+
path: "/api/auth/login",
|
|
34
|
+
description: "Authenticate and return JWT",
|
|
35
|
+
auth: false,
|
|
36
|
+
successStatus: 200,
|
|
37
|
+
successDescription: "JWT token",
|
|
38
|
+
request: { body: { email: "string", password: "string" } },
|
|
39
|
+
errors: [
|
|
40
|
+
{ status: 401, code: "INVALID_CREDENTIALS", description: "Bad password" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
behaviors: [
|
|
45
|
+
{
|
|
46
|
+
id: "BHV-001",
|
|
47
|
+
description: "Rate-limit login to 5 attempts per minute",
|
|
48
|
+
trigger: "POST /api/auth/login",
|
|
49
|
+
constraints: ["block after 5 failures"],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function makeProvider(response: string): AIProvider {
|
|
55
|
+
return { generate: vi.fn().mockResolvedValue(response) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── dslFilePath ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe("dslFilePath", () => {
|
|
61
|
+
it("replaces .md extension with .dsl.json", () => {
|
|
62
|
+
expect(dslFilePath("/specs/feature-login-v1.md")).toBe("/specs/feature-login-v1.dsl.json");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("works with relative paths", () => {
|
|
66
|
+
expect(dslFilePath("specs/my-feature-v2.md")).toBe("specs/my-feature-v2.dsl.json");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("handles files in the current directory", () => {
|
|
70
|
+
expect(dslFilePath("feature.md")).toBe("feature.dsl.json");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("preserves directory structure", () => {
|
|
74
|
+
const result = dslFilePath("/a/b/c/feature-v3.md");
|
|
75
|
+
expect(result).toContain("/a/b/c/");
|
|
76
|
+
expect(result.endsWith(".dsl.json")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ─── buildDslContextSection ───────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
describe("buildDslContextSection", () => {
|
|
83
|
+
it("includes the section header and footer", () => {
|
|
84
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
85
|
+
expect(result).toContain("=== Feature DSL");
|
|
86
|
+
expect(result).toContain("=== End of DSL ===");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("lists model names and fields", () => {
|
|
90
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
91
|
+
expect(result).toContain("User:");
|
|
92
|
+
expect(result).toContain("email: String");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("marks required fields", () => {
|
|
96
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
97
|
+
expect(result).toContain("required");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("marks unique fields", () => {
|
|
101
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
102
|
+
expect(result).toContain("unique");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("includes model relations", () => {
|
|
106
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
107
|
+
expect(result).toContain("has many Session");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("includes endpoint method, path, and auth", () => {
|
|
111
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
112
|
+
expect(result).toContain("POST");
|
|
113
|
+
expect(result).toContain("/api/auth/login");
|
|
114
|
+
expect(result).toContain("auth: false");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("includes endpoint error codes", () => {
|
|
118
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
119
|
+
expect(result).toContain("INVALID_CREDENTIALS");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("includes request body fields", () => {
|
|
123
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
124
|
+
expect(result).toContain("email");
|
|
125
|
+
expect(result).toContain("password");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("includes behaviors with trigger and constraints", () => {
|
|
129
|
+
const result = buildDslContextSection(VALID_DSL);
|
|
130
|
+
expect(result).toContain("Rate-limit login");
|
|
131
|
+
expect(result).toContain("POST /api/auth/login");
|
|
132
|
+
expect(result).toContain("block after 5 failures");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("handles empty models array gracefully", () => {
|
|
136
|
+
const dsl: SpecDSL = { ...VALID_DSL, models: [] };
|
|
137
|
+
const result = buildDslContextSection(dsl);
|
|
138
|
+
expect(result).not.toContain("-- Data Models --");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("handles empty endpoints array gracefully", () => {
|
|
142
|
+
const dsl: SpecDSL = { ...VALID_DSL, endpoints: [] };
|
|
143
|
+
const result = buildDslContextSection(dsl);
|
|
144
|
+
expect(result).not.toContain("-- API Endpoints --");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("handles empty behaviors array gracefully", () => {
|
|
148
|
+
const dsl: SpecDSL = { ...VALID_DSL, behaviors: [] };
|
|
149
|
+
const result = buildDslContextSection(dsl);
|
|
150
|
+
expect(result).not.toContain("-- Business Behaviors --");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("includes UI components section when components are present", () => {
|
|
154
|
+
const dsl: SpecDSL = {
|
|
155
|
+
...VALID_DSL,
|
|
156
|
+
components: [
|
|
157
|
+
{
|
|
158
|
+
id: "CMP-001",
|
|
159
|
+
name: "LoginForm",
|
|
160
|
+
description: "Login form component",
|
|
161
|
+
props: [{ name: "onSuccess", type: "() => void", required: true }],
|
|
162
|
+
events: [{ name: "onSubmit", payload: "FormData" }],
|
|
163
|
+
state: { isLoading: "boolean" },
|
|
164
|
+
apiCalls: ["EP-001"],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
const result = buildDslContextSection(dsl);
|
|
169
|
+
expect(result).toContain("-- UI Components --");
|
|
170
|
+
expect(result).toContain("LoginForm");
|
|
171
|
+
expect(result).toContain("onSuccess");
|
|
172
|
+
expect(result).toContain("onSubmit");
|
|
173
|
+
expect(result).toContain("isLoading:boolean");
|
|
174
|
+
expect(result).toContain("EP-001");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ─── DslExtractor.extract — success path ─────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe("DslExtractor.extract — success", () => {
|
|
181
|
+
it("returns a valid SpecDSL when AI output is bare JSON", async () => {
|
|
182
|
+
const provider = makeProvider(JSON.stringify(VALID_DSL));
|
|
183
|
+
const extractor = new DslExtractor(provider);
|
|
184
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
185
|
+
const result = await extractor.extract("spec content", { auto: true });
|
|
186
|
+
consoleSpy.mockRestore();
|
|
187
|
+
expect(result).not.toBeNull();
|
|
188
|
+
expect(result?.feature.id).toBe("user-login");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns a valid SpecDSL when AI wraps output in a JSON fence", async () => {
|
|
192
|
+
const fenced = "```json\n" + JSON.stringify(VALID_DSL) + "\n```";
|
|
193
|
+
const provider = makeProvider(fenced);
|
|
194
|
+
const extractor = new DslExtractor(provider);
|
|
195
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
196
|
+
const result = await extractor.extract("spec content", { auto: true });
|
|
197
|
+
consoleSpy.mockRestore();
|
|
198
|
+
expect(result?.feature.title).toBe("User Login");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("sanitizes empty error entries before validation", async () => {
|
|
202
|
+
const dslWithEmptyErrors = {
|
|
203
|
+
...VALID_DSL,
|
|
204
|
+
endpoints: [
|
|
205
|
+
{
|
|
206
|
+
...VALID_DSL.endpoints[0],
|
|
207
|
+
errors: [
|
|
208
|
+
{ status: 400, code: "", description: "" }, // invalid — should be stripped
|
|
209
|
+
{ status: 401, code: "INVALID_CREDENTIALS", description: "Bad password" },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
const provider = makeProvider(JSON.stringify(dslWithEmptyErrors));
|
|
215
|
+
const extractor = new DslExtractor(provider);
|
|
216
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
217
|
+
const result = await extractor.extract("spec content", { auto: true });
|
|
218
|
+
consoleSpy.mockRestore();
|
|
219
|
+
expect(result).not.toBeNull();
|
|
220
|
+
// The empty error entry should have been stripped
|
|
221
|
+
expect(result?.endpoints[0].errors).toHaveLength(1);
|
|
222
|
+
expect(result?.endpoints[0].errors?.[0].code).toBe("INVALID_CREDENTIALS");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ─── DslExtractor.extract — failure paths ────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
describe("DslExtractor.extract — failure / auto mode", () => {
|
|
229
|
+
it("returns null in auto mode when AI returns invalid JSON", async () => {
|
|
230
|
+
const provider = makeProvider("Not JSON at all");
|
|
231
|
+
const extractor = new DslExtractor(provider);
|
|
232
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
233
|
+
const result = await extractor.extract("spec", { auto: true });
|
|
234
|
+
consoleSpy.mockRestore();
|
|
235
|
+
expect(result).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("returns null in auto mode when provider throws", async () => {
|
|
239
|
+
const provider: AIProvider = { generate: vi.fn().mockRejectedValue(new Error("network")) };
|
|
240
|
+
const extractor = new DslExtractor(provider);
|
|
241
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
242
|
+
const result = await extractor.extract("spec", { auto: true });
|
|
243
|
+
consoleSpy.mockRestore();
|
|
244
|
+
expect(result).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("retries when first attempt produces invalid DSL (missing required field)", async () => {
|
|
248
|
+
// First response: invalid DSL (missing feature.description)
|
|
249
|
+
const badDsl = { ...VALID_DSL, feature: { id: "x", title: "X", description: "" } };
|
|
250
|
+
// Second response: valid DSL
|
|
251
|
+
const provider: AIProvider = {
|
|
252
|
+
generate: vi.fn()
|
|
253
|
+
.mockResolvedValueOnce(JSON.stringify(badDsl))
|
|
254
|
+
.mockResolvedValueOnce(JSON.stringify(VALID_DSL)),
|
|
255
|
+
};
|
|
256
|
+
const extractor = new DslExtractor(provider);
|
|
257
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
258
|
+
const result = await extractor.extract("spec", { auto: true });
|
|
259
|
+
consoleSpy.mockRestore();
|
|
260
|
+
// Should have retried — provider called at least twice
|
|
261
|
+
expect((provider.generate as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
262
|
+
expect(result?.feature.id).toBe("user-login");
|
|
263
|
+
});
|
|
264
|
+
});
|