ai-spec-dev 0.38.0 → 0.41.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.
Files changed (66) hide show
  1. package/RELEASE_LOG.md +231 -0
  2. package/cli/commands/create.ts +9 -1176
  3. package/cli/commands/dashboard.ts +1 -1
  4. package/cli/pipeline/helpers.ts +34 -0
  5. package/cli/pipeline/multi-repo.ts +483 -0
  6. package/cli/pipeline/single-repo.ts +755 -0
  7. package/cli/utils.ts +2 -0
  8. package/core/code-generator.ts +52 -341
  9. package/core/codegen/helpers.ts +219 -0
  10. package/core/codegen/topo-sort.ts +98 -0
  11. package/core/constitution-consolidator.ts +2 -2
  12. package/core/dsl-coverage-checker.ts +298 -0
  13. package/core/dsl-extractor.ts +19 -46
  14. package/core/dsl-feedback.ts +1 -1
  15. package/core/dsl-validator.ts +74 -0
  16. package/core/error-feedback.ts +95 -11
  17. package/core/frontend-context-loader.ts +27 -5
  18. package/core/knowledge-memory.ts +52 -0
  19. package/core/mock/fixtures.ts +89 -0
  20. package/core/mock/proxy.ts +380 -0
  21. package/core/mock-server-generator.ts +12 -460
  22. package/core/requirement-decomposer.ts +4 -28
  23. package/core/reviewer.ts +1 -1
  24. package/core/safe-json.ts +76 -0
  25. package/core/spec-updater.ts +5 -21
  26. package/core/token-budget.ts +124 -0
  27. package/core/vcr.ts +20 -1
  28. package/dist/cli/index.js +4110 -3534
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/index.mjs +4237 -3661
  31. package/dist/cli/index.mjs.map +1 -1
  32. package/dist/index.d.mts +18 -16
  33. package/dist/index.d.ts +18 -16
  34. package/dist/index.js +310 -182
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +308 -180
  37. package/dist/index.mjs.map +1 -1
  38. package/package.json +2 -2
  39. package/purpose.md +173 -33
  40. package/tests/auto-consolidation.test.ts +109 -0
  41. package/tests/combined-generator.test.ts +81 -0
  42. package/tests/constitution-consolidator.test.ts +161 -0
  43. package/tests/constitution-generator.test.ts +94 -0
  44. package/tests/contract-bridge.test.ts +201 -0
  45. package/tests/design-dialogue.test.ts +108 -0
  46. package/tests/dsl-coverage-checker.test.ts +230 -0
  47. package/tests/dsl-feedback.test.ts +45 -0
  48. package/tests/dsl-validator-xref.test.ts +99 -0
  49. package/tests/error-feedback-repair.test.ts +319 -0
  50. package/tests/error-feedback-validation.test.ts +91 -0
  51. package/tests/frontend-context-loader.test.ts +609 -0
  52. package/tests/global-constitution.test.ts +110 -0
  53. package/tests/key-store.test.ts +73 -0
  54. package/tests/knowledge-memory.test.ts +327 -0
  55. package/tests/project-index.test.ts +206 -0
  56. package/tests/prompt-hasher.test.ts +19 -0
  57. package/tests/requirement-decomposer.test.ts +171 -0
  58. package/tests/reviewer.test.ts +4 -1
  59. package/tests/run-logger.test.ts +289 -0
  60. package/tests/run-snapshot.test.ts +113 -0
  61. package/tests/safe-json.test.ts +63 -0
  62. package/tests/spec-updater.test.ts +161 -0
  63. package/tests/test-generator.test.ts +146 -0
  64. package/tests/token-budget.test.ts +124 -0
  65. package/tests/vcr-hash.test.ts +101 -0
  66. package/tests/workspace-loader.test.ts +277 -0
@@ -0,0 +1,230 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ extractKeywords,
4
+ extractSpecRequirements,
5
+ checkDslCoverage,
6
+ SpecRequirement,
7
+ } from "../core/dsl-coverage-checker";
8
+ import { SpecDSL } from "../core/dsl-types";
9
+
10
+ // ─── extractKeywords ────────────────────────────────────────────────────────
11
+
12
+ describe("extractKeywords", () => {
13
+ it("extracts English words", () => {
14
+ const kw = extractKeywords("Create order with payment");
15
+ expect(kw.has("create")).toBe(true);
16
+ expect(kw.has("order")).toBe(true);
17
+ expect(kw.has("payment")).toBe(true);
18
+ });
19
+
20
+ it("filters English stopwords", () => {
21
+ const kw = extractKeywords("the user is able to get a list");
22
+ expect(kw.has("the")).toBe(false);
23
+ expect(kw.has("is")).toBe(false);
24
+ expect(kw.has("list")).toBe(true);
25
+ });
26
+
27
+ it("extracts CJK characters and bigrams", () => {
28
+ const kw = extractKeywords("订单管理系统");
29
+ expect(kw.has("订")).toBe(true);
30
+ expect(kw.has("订单")).toBe(true);
31
+ expect(kw.has("管理")).toBe(true);
32
+ });
33
+
34
+ it("handles mixed CJK + English", () => {
35
+ const kw = extractKeywords("创建 order API 接口");
36
+ expect(kw.has("order")).toBe(true);
37
+ expect(kw.has("api")).toBe(true);
38
+ expect(kw.has("创建")).toBe(true);
39
+ expect(kw.has("接口")).toBe(true);
40
+ });
41
+
42
+ it("filters CJK stopwords", () => {
43
+ const kw = extractKeywords("用户可以使用系统");
44
+ // "用户", "可以", "使用", "系统" are all stopwords
45
+ expect(kw.has("用户")).toBe(false);
46
+ expect(kw.has("可以")).toBe(false);
47
+ });
48
+
49
+ it("returns empty set for empty input", () => {
50
+ expect(extractKeywords("").size).toBe(0);
51
+ });
52
+ });
53
+
54
+ // ─── extractSpecRequirements ────────────────────────────────────────────────
55
+
56
+ describe("extractSpecRequirements", () => {
57
+ it("extracts Chinese user stories", () => {
58
+ const spec = `## 3. 用户故事
59
+ - 作为 **管理员**,我希望 **查看所有订单**,以便 **管理业务**
60
+ - 作为 **客户**,我希望 **提交订单**,以便 **购买商品**`;
61
+
62
+ const reqs = extractSpecRequirements(spec);
63
+ const stories = reqs.filter((r) => r.section === "user_story");
64
+ expect(stories.length).toBe(2);
65
+ expect(stories[0].id).toBe("US-1");
66
+ expect(stories[0].text).toContain("管理员");
67
+ });
68
+
69
+ it("extracts English user stories", () => {
70
+ const spec = `## 3. User Stories
71
+ - As a **manager**, I want to **view all orders**, so that **I can manage business**
72
+ - As an **admin**, I want to **delete users**`;
73
+
74
+ const reqs = extractSpecRequirements(spec);
75
+ const stories = reqs.filter((r) => r.section === "user_story");
76
+ expect(stories.length).toBe(2);
77
+ });
78
+
79
+ it("extracts functional requirements from checklist", () => {
80
+ const spec = `## 4. 功能需求
81
+ - [ ] 创建订单接口,支持多商品
82
+ - [ ] 订单状态流转(待支付→已支付→已发货→已完成)
83
+ - [x] 查询订单列表,支持分页
84
+ ## 5. API 设计`;
85
+
86
+ const reqs = extractSpecRequirements(spec);
87
+ const frs = reqs.filter((r) => r.section === "functional_req");
88
+ expect(frs.length).toBe(3);
89
+ expect(frs[0].text).toContain("创建订单");
90
+ });
91
+
92
+ it("extracts numbered sub-items", () => {
93
+ const spec = `### 4. Functional Requirements
94
+ 4.1.1 Users can register with email and password
95
+ 4.1.2 Email verification is required
96
+ ## 5. API`;
97
+
98
+ const reqs = extractSpecRequirements(spec);
99
+ const frs = reqs.filter((r) => r.section === "functional_req");
100
+ expect(frs.length).toBe(2);
101
+ });
102
+
103
+ it("extracts boundary conditions", () => {
104
+ const spec = `### 边界条件
105
+ - 当订单金额为0时,应返回错误
106
+ - 库存不足时,应提示用户
107
+ ## 5. 下一节`;
108
+
109
+ const reqs = extractSpecRequirements(spec);
110
+ const bcs = reqs.filter((r) => r.section === "boundary_condition");
111
+ expect(bcs.length).toBe(2);
112
+ expect(bcs[0].id).toBe("BC-1");
113
+ });
114
+
115
+ it("returns empty for spec with no requirements", () => {
116
+ const reqs = extractSpecRequirements("# Overview\nSome description.");
117
+ expect(reqs.length).toBe(0);
118
+ });
119
+ });
120
+
121
+ // ─── checkDslCoverage ───────────────────────────────────────────────────────
122
+
123
+ function makeDsl(overrides: Partial<SpecDSL> = {}): SpecDSL {
124
+ return {
125
+ version: "1.0",
126
+ feature: { id: "test", title: "Test Feature", description: "A test" },
127
+ models: [],
128
+ endpoints: [],
129
+ behaviors: [],
130
+ ...overrides,
131
+ };
132
+ }
133
+
134
+ describe("checkDslCoverage", () => {
135
+ it("returns 1.0 coverage when no requirements", () => {
136
+ const result = checkDslCoverage([], makeDsl());
137
+ expect(result.coverageRatio).toBe(1.0);
138
+ });
139
+
140
+ it("detects covered requirements via endpoint description match", () => {
141
+ const reqs: SpecRequirement[] = [
142
+ { id: "US-1", text: "查看所有订单列表", section: "user_story" },
143
+ ];
144
+ const dsl = makeDsl({
145
+ endpoints: [{
146
+ id: "EP-001", method: "GET", path: "/orders",
147
+ description: "获取订单列表,支持分页查询",
148
+ auth: true, successStatus: 200, successDescription: "ok",
149
+ }],
150
+ });
151
+
152
+ const result = checkDslCoverage(reqs, dsl);
153
+ expect(result.coverageRatio).toBe(1.0);
154
+ expect(result.covered.length).toBe(1);
155
+ });
156
+
157
+ it("detects uncovered requirements", () => {
158
+ const reqs: SpecRequirement[] = [
159
+ { id: "US-1", text: "export data to Excel spreadsheet", section: "user_story" },
160
+ { id: "US-2", text: "view order details", section: "user_story" },
161
+ ];
162
+ const dsl = makeDsl({
163
+ endpoints: [{
164
+ id: "EP-001", method: "GET", path: "/orders/:id",
165
+ description: "Get order details by ID",
166
+ auth: true, successStatus: 200, successDescription: "ok",
167
+ }],
168
+ });
169
+
170
+ const result = checkDslCoverage(reqs, dsl);
171
+ expect(result.uncovered.length).toBe(1);
172
+ expect(result.uncovered[0].id).toBe("US-1");
173
+ expect(result.coverageRatio).toBe(0.5);
174
+ });
175
+
176
+ it("matches via model field names", () => {
177
+ const reqs: SpecRequirement[] = [
178
+ { id: "FR-1", text: "record payment amount and payment method", section: "functional_req" },
179
+ ];
180
+ const dsl = makeDsl({
181
+ models: [{
182
+ name: "Payment",
183
+ fields: [
184
+ { name: "amount", type: "Float", required: true },
185
+ { name: "method", type: "String", required: true, description: "payment method" },
186
+ ],
187
+ }],
188
+ });
189
+
190
+ const result = checkDslCoverage(reqs, dsl);
191
+ expect(result.coverageRatio).toBe(1.0);
192
+ });
193
+
194
+ it("matches via behavior descriptions", () => {
195
+ const reqs: SpecRequirement[] = [
196
+ { id: "FR-1", text: "send email notification after order confirmed", section: "functional_req" },
197
+ ];
198
+ const dsl = makeDsl({
199
+ behaviors: [{
200
+ id: "BHV-001",
201
+ description: "Send email notification when order status changes to confirmed",
202
+ trigger: "order.confirmed",
203
+ }],
204
+ });
205
+
206
+ const result = checkDslCoverage(reqs, dsl);
207
+ expect(result.coverageRatio).toBe(1.0);
208
+ });
209
+
210
+ it("reports low coverage ratio correctly", () => {
211
+ const reqs: SpecRequirement[] = [
212
+ { id: "US-1", text: "manage user permissions", section: "user_story" },
213
+ { id: "US-2", text: "upload file attachments", section: "user_story" },
214
+ { id: "US-3", text: "generate monthly reports", section: "user_story" },
215
+ { id: "US-4", text: "schedule automated tasks", section: "user_story" },
216
+ { id: "US-5", text: "configure system settings", section: "user_story" },
217
+ ];
218
+ const dsl = makeDsl({
219
+ endpoints: [{
220
+ id: "EP-001", method: "GET", path: "/settings",
221
+ description: "Get system settings and configuration",
222
+ auth: true, successStatus: 200, successDescription: "ok",
223
+ }],
224
+ });
225
+
226
+ const result = checkDslCoverage(reqs, dsl);
227
+ expect(result.coverageRatio).toBeLessThan(0.8);
228
+ expect(result.uncovered.length).toBeGreaterThan(0);
229
+ });
230
+ });
@@ -238,6 +238,51 @@ describe("extractStructuralFindings", () => {
238
238
  expect(f.description.length).toBeGreaterThan(0);
239
239
  }
240
240
  });
241
+
242
+ it("parses structured JSON block from review text", () => {
243
+ const pass1 = `## Architecture
244
+ Score: 5/10
245
+
246
+ ## 🔍 结构性发现 JSON
247
+ \`\`\`json
248
+ {
249
+ "structuralFindings": [
250
+ { "category": "auth_design", "description": "POST /admin lacks auth" },
251
+ { "category": "model_design", "description": "User model missing email field" }
252
+ ]
253
+ }
254
+ \`\`\``;
255
+ const findings = extractStructuralFindings(`${pass1}\n${SEP}\nimpl`);
256
+ expect(findings).toHaveLength(2);
257
+ expect(findings[0].category).toBe("auth_design");
258
+ expect(findings[1].category).toBe("model_design");
259
+ });
260
+
261
+ it("filters invalid entries from JSON block", () => {
262
+ const pass1 = `Score: 5/10
263
+ \`\`\`json
264
+ {
265
+ "structuralFindings": [
266
+ { "category": "auth_design", "description": "valid" },
267
+ { "bad": true },
268
+ "not an object"
269
+ ]
270
+ }
271
+ \`\`\``;
272
+ const findings = extractStructuralFindings(pass1);
273
+ expect(findings).toHaveLength(1);
274
+ expect(findings[0].description).toBe("valid");
275
+ });
276
+
277
+ it("falls back to regex when JSON is malformed", () => {
278
+ const pass1 = `Score: 5/10
279
+ \`\`\`json
280
+ { broken json!!!
281
+ \`\`\`
282
+ Several endpoints have missing auth requirements.`;
283
+ const findings = extractStructuralFindings(pass1);
284
+ expect(findings.some((f) => f.category === "auth_design")).toBe(true);
285
+ });
241
286
  });
242
287
 
243
288
  // ─── Prompt builders (smoke tests) ────────────────────────────────────────────
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateDsl } from "../core/dsl-validator";
3
+
4
+ /** Minimal valid DSL for testing cross-reference checks. */
5
+ function baseDsl(overrides: Record<string, unknown> = {}) {
6
+ return {
7
+ version: "1.0",
8
+ feature: { id: "f1", title: "Test", description: "test" },
9
+ models: [
10
+ { name: "User", fields: [{ name: "id", type: "string", required: true }] },
11
+ { name: "Post", fields: [{ name: "id", type: "string", required: true }], relations: ["User hasMany Post"] },
12
+ ],
13
+ endpoints: [
14
+ {
15
+ id: "get-users", method: "GET", path: "/users", description: "List users",
16
+ auth: false, successStatus: 200, successDescription: "ok",
17
+ },
18
+ ],
19
+ behaviors: [],
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ describe("DSL validator cross-reference checks", () => {
25
+ it("detects duplicate path+method", () => {
26
+ const dsl = baseDsl({
27
+ endpoints: [
28
+ { id: "ep1", method: "GET", path: "/users", description: "a", auth: false, successStatus: 200, successDescription: "ok" },
29
+ { id: "ep2", method: "GET", path: "/users", description: "b", auth: false, successStatus: 200, successDescription: "ok" },
30
+ ],
31
+ });
32
+ const result = validateDsl(dsl);
33
+ expect(result.valid).toBe(false);
34
+ expect(result.errors?.some((e) => e.message.includes("Duplicate route"))).toBe(true);
35
+ });
36
+
37
+ it("allows same path with different methods", () => {
38
+ const dsl = baseDsl({
39
+ endpoints: [
40
+ { id: "ep1", method: "GET", path: "/users", description: "a", auth: false, successStatus: 200, successDescription: "ok" },
41
+ { id: "ep2", method: "POST", path: "/users", description: "b", auth: false, successStatus: 201, successDescription: "ok" },
42
+ ],
43
+ });
44
+ const result = validateDsl(dsl);
45
+ expect(result.valid).toBe(true);
46
+ });
47
+
48
+ it("detects relation referencing non-existent model", () => {
49
+ const dsl = baseDsl({
50
+ models: [
51
+ {
52
+ name: "User",
53
+ fields: [{ name: "id", type: "string", required: true }],
54
+ relations: ["User hasMany Comment"],
55
+ },
56
+ ],
57
+ });
58
+ const result = validateDsl(dsl);
59
+ expect(result.valid).toBe(false);
60
+ expect(result.errors?.some((e) => e.message.includes('"Comment"'))).toBe(true);
61
+ });
62
+
63
+ it("passes when relation references existing model", () => {
64
+ const dsl = baseDsl(); // User hasMany Post — both models exist
65
+ const result = validateDsl(dsl);
66
+ expect(result.valid).toBe(true);
67
+ });
68
+
69
+ it("detects component apiCalls referencing non-existent endpoint", () => {
70
+ const dsl = baseDsl({
71
+ components: [
72
+ {
73
+ id: "c1", name: "UserList", description: "Shows users",
74
+ props: [{ name: "limit", type: "number", required: false }],
75
+ events: [{ name: "select" }],
76
+ apiCalls: ["get-users", "nonexistent-endpoint"],
77
+ },
78
+ ],
79
+ });
80
+ const result = validateDsl(dsl);
81
+ expect(result.valid).toBe(false);
82
+ expect(result.errors?.some((e) => e.message.includes('"nonexistent-endpoint"'))).toBe(true);
83
+ });
84
+
85
+ it("passes when component apiCalls reference existing endpoints", () => {
86
+ const dsl = baseDsl({
87
+ components: [
88
+ {
89
+ id: "c1", name: "UserList", description: "Shows users",
90
+ props: [{ name: "limit", type: "number", required: false }],
91
+ events: [{ name: "select" }],
92
+ apiCalls: ["get-users"],
93
+ },
94
+ ],
95
+ });
96
+ const result = validateDsl(dsl);
97
+ expect(result.valid).toBe(true);
98
+ });
99
+ });