ai-spec-dev 0.31.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-spec-dev",
3
- "version": "0.31.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
  }
@@ -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
+ });
@@ -0,0 +1,266 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ assessDslRichness,
4
+ extractStructuralFindings,
5
+ buildDslGapRefinementPrompt,
6
+ buildStructuralAmendmentPrompt,
7
+ } from "../core/dsl-feedback";
8
+ import type { SpecDSL } from "../core/dsl-types";
9
+
10
+ // ─── Fixtures ─────────────────────────────────────────────────────────────────
11
+
12
+ function makeEndpoint(overrides: Partial<SpecDSL["endpoints"][0]> = {}): SpecDSL["endpoints"][0] {
13
+ return {
14
+ id: "EP-001",
15
+ method: "GET",
16
+ path: "/api/items",
17
+ description: "Returns a paginated list of items with filtering support",
18
+ auth: true,
19
+ successStatus: 200,
20
+ successDescription: "List of items returned",
21
+ errors: [{ status: 401, code: "UNAUTHORIZED", description: "Missing auth token" }],
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ function makeModel(overrides: Partial<SpecDSL["models"][0]> = {}): SpecDSL["models"][0] {
27
+ return {
28
+ name: "Item",
29
+ description: "An inventory item",
30
+ fields: [
31
+ { name: "id", type: "String", required: true },
32
+ { name: "name", type: "String", required: true },
33
+ { name: "price", type: "Float", required: true },
34
+ ],
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ function makeDsl(overrides: Partial<SpecDSL> = {}): SpecDSL {
40
+ return {
41
+ version: "1.0",
42
+ feature: { id: "items", title: "Items", description: "Item management" },
43
+ models: [makeModel()],
44
+ endpoints: [makeEndpoint()],
45
+ behaviors: [],
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ // ─── assessDslRichness ────────────────────────────────────────────────────────
51
+
52
+ describe("assessDslRichness", () => {
53
+ it("returns no_models_no_endpoints when DSL is completely empty", () => {
54
+ const dsl = makeDsl({ endpoints: [], models: [] });
55
+ const gaps = assessDslRichness(dsl);
56
+ expect(gaps).toHaveLength(1);
57
+ expect(gaps[0].code).toBe("no_models_no_endpoints");
58
+ });
59
+
60
+ it("returns early with only no_models_no_endpoints when empty (no further checks)", () => {
61
+ const dsl = makeDsl({ endpoints: [], models: [] });
62
+ const gaps = assessDslRichness(dsl);
63
+ // Should not also report sparse_model / missing_errors when empty
64
+ expect(gaps.every((g) => g.code === "no_models_no_endpoints")).toBe(true);
65
+ });
66
+
67
+ it("returns no gaps for a well-formed DSL", () => {
68
+ const dsl = makeDsl();
69
+ const gaps = assessDslRichness(dsl);
70
+ expect(gaps).toHaveLength(0);
71
+ });
72
+
73
+ it("detects generic_endpoint_desc when description is too short", () => {
74
+ const dsl = makeDsl({
75
+ endpoints: [makeEndpoint({ description: "Get items" })], // < 15 chars
76
+ });
77
+ const gaps = assessDslRichness(dsl);
78
+ expect(gaps.some((g) => g.code === "generic_endpoint_desc")).toBe(true);
79
+ });
80
+
81
+ it("detects generic_endpoint_desc when description starts with 'handles'", () => {
82
+ const dsl = makeDsl({
83
+ endpoints: [makeEndpoint({ description: "Handles the item request and processing" })],
84
+ });
85
+ const gaps = assessDslRichness(dsl);
86
+ expect(gaps.some((g) => g.code === "generic_endpoint_desc")).toBe(true);
87
+ });
88
+
89
+ it("detects generic_endpoint_desc for Chinese generic verbs (管理)", () => {
90
+ const dsl = makeDsl({
91
+ endpoints: [makeEndpoint({ description: "管理商品列表的接口调用和返回" })],
92
+ });
93
+ const gaps = assessDslRichness(dsl);
94
+ expect(gaps.some((g) => g.code === "generic_endpoint_desc")).toBe(true);
95
+ });
96
+
97
+ it("does NOT flag a sufficiently descriptive endpoint", () => {
98
+ const dsl = makeDsl({
99
+ endpoints: [makeEndpoint({ description: "Returns paginated list of active inventory items filtered by category" })],
100
+ });
101
+ const gaps = assessDslRichness(dsl);
102
+ expect(gaps.some((g) => g.code === "generic_endpoint_desc")).toBe(false);
103
+ });
104
+
105
+ it("detects missing_errors when all endpoints lack error definitions (≥2 endpoints)", () => {
106
+ const ep = makeEndpoint({ errors: undefined });
107
+ const dsl = makeDsl({ endpoints: [ep, { ...ep, id: "EP-002", path: "/api/items/:id" }] });
108
+ const gaps = assessDslRichness(dsl);
109
+ expect(gaps.some((g) => g.code === "missing_errors")).toBe(true);
110
+ });
111
+
112
+ it("does NOT flag missing_errors when there is only one endpoint", () => {
113
+ const dsl = makeDsl({
114
+ endpoints: [makeEndpoint({ errors: undefined })],
115
+ });
116
+ const gaps = assessDslRichness(dsl);
117
+ expect(gaps.some((g) => g.code === "missing_errors")).toBe(false);
118
+ });
119
+
120
+ it("does NOT flag missing_errors when at least one endpoint has errors", () => {
121
+ const withErrors = makeEndpoint();
122
+ const withoutErrors = makeEndpoint({ id: "EP-002", path: "/api/items/:id", errors: undefined });
123
+ const dsl = makeDsl({ endpoints: [withErrors, withoutErrors] });
124
+ const gaps = assessDslRichness(dsl);
125
+ expect(gaps.some((g) => g.code === "missing_errors")).toBe(false);
126
+ });
127
+
128
+ it("detects sparse_model when model has fewer than 2 fields", () => {
129
+ const dsl = makeDsl({
130
+ models: [makeModel({ fields: [{ name: "id", type: "String", required: true }] })],
131
+ });
132
+ const gaps = assessDslRichness(dsl);
133
+ expect(gaps.some((g) => g.code === "sparse_model")).toBe(true);
134
+ });
135
+
136
+ it("detects sparse_model when model has zero fields", () => {
137
+ const dsl = makeDsl({ models: [makeModel({ fields: [] })] });
138
+ const gaps = assessDslRichness(dsl);
139
+ expect(gaps.some((g) => g.code === "sparse_model")).toBe(true);
140
+ });
141
+
142
+ it("does NOT flag sparse_model when model has 2 or more fields", () => {
143
+ const dsl = makeDsl({
144
+ models: [makeModel({
145
+ fields: [
146
+ { name: "id", type: "String", required: true },
147
+ { name: "name", type: "String", required: true },
148
+ ],
149
+ })],
150
+ });
151
+ const gaps = assessDslRichness(dsl);
152
+ expect(gaps.some((g) => g.code === "sparse_model")).toBe(false);
153
+ });
154
+
155
+ it("can detect multiple gaps simultaneously", () => {
156
+ const dsl = makeDsl({
157
+ endpoints: [makeEndpoint({ description: "处理", errors: undefined }), makeEndpoint({ id: "EP-002", path: "/b", errors: undefined })],
158
+ models: [makeModel({ fields: [{ name: "id", type: "String", required: true }] })],
159
+ });
160
+ const codes = assessDslRichness(dsl).map((g) => g.code);
161
+ expect(codes).toContain("generic_endpoint_desc");
162
+ expect(codes).toContain("missing_errors");
163
+ expect(codes).toContain("sparse_model");
164
+ });
165
+
166
+ it("gap hint is a non-empty string", () => {
167
+ const dsl = makeDsl({ endpoints: [], models: [] });
168
+ const gaps = assessDslRichness(dsl);
169
+ for (const gap of gaps) {
170
+ expect(typeof gap.hint).toBe("string");
171
+ expect(gap.hint.length).toBeGreaterThan(0);
172
+ }
173
+ });
174
+ });
175
+
176
+ // ─── extractStructuralFindings ────────────────────────────────────────────────
177
+
178
+ describe("extractStructuralFindings", () => {
179
+ const SEP = "─".repeat(52);
180
+
181
+ it("returns empty array for empty review text", () => {
182
+ expect(extractStructuralFindings("")).toHaveLength(0);
183
+ });
184
+
185
+ it("returns empty array when Pass 1 scores ≥ 8", () => {
186
+ const reviewText = `Architecture looks solid.\nScore: 9/10\n${SEP}\nimpl stuff`;
187
+ expect(extractStructuralFindings(reviewText)).toHaveLength(0);
188
+ });
189
+
190
+ it("detects auth_design finding from Chinese pattern", () => {
191
+ const pass1 = `The endpoint /api/users/create 缺少认证,应该加上 JWT 验证。\nScore: 5/10`;
192
+ const reviewText = `${pass1}\n${SEP}\nimpl notes`;
193
+ const findings = extractStructuralFindings(reviewText);
194
+ expect(findings.some((f) => f.category === "auth_design")).toBe(true);
195
+ });
196
+
197
+ it("detects auth_design finding from English pattern", () => {
198
+ const pass1 = `The POST /orders endpoint is missing auth — it should require authentication.\nScore: 6/10`;
199
+ const reviewText = `${pass1}\n${SEP}\nimpl notes`;
200
+ const findings = extractStructuralFindings(reviewText);
201
+ expect(findings.some((f) => f.category === "auth_design")).toBe(true);
202
+ });
203
+
204
+ it("detects api_contract finding", () => {
205
+ const pass1 = `接口设计问题:response 缺少 pagination 字段。\nScore: 6/10`;
206
+ const reviewText = `${pass1}\n${SEP}\nimpl notes`;
207
+ const findings = extractStructuralFindings(reviewText);
208
+ expect(findings.some((f) => f.category === "api_contract")).toBe(true);
209
+ });
210
+
211
+ it("detects model_design finding", () => {
212
+ const pass1 = `模型缺少字段:Order model 没有 status 和 total 字段。\nScore: 5/10`;
213
+ const reviewText = `${pass1}\n${SEP}\nimpl notes`;
214
+ const findings = extractStructuralFindings(reviewText);
215
+ expect(findings.some((f) => f.category === "model_design")).toBe(true);
216
+ });
217
+
218
+ it("detects layer_violation finding", () => {
219
+ const pass1 = `分层问题:business logic 直接写在 Controller 里,违反了 Service 层设计。\nScore: 4/10`;
220
+ const reviewText = `${pass1}\n${SEP}\nimpl notes`;
221
+ const findings = extractStructuralFindings(reviewText);
222
+ expect(findings.some((f) => f.category === "layer_violation")).toBe(true);
223
+ });
224
+
225
+ it("returns empty when review text has no structural keywords but low score", () => {
226
+ const pass1 = `Code is a bit messy but structurally OK. Some variable names are unclear.\nScore: 6/10`;
227
+ const reviewText = `${pass1}\n${SEP}\nimpl notes`;
228
+ const findings = extractStructuralFindings(reviewText);
229
+ expect(findings).toHaveLength(0);
230
+ });
231
+
232
+ it("each finding has a non-empty description", () => {
233
+ const pass1 = `缺少认证在 /api/admin endpoint 上。模型缺少字段 role。\nScore: 5/10`;
234
+ const reviewText = `${pass1}\n${SEP}\nimpl`;
235
+ const findings = extractStructuralFindings(reviewText);
236
+ for (const f of findings) {
237
+ expect(typeof f.description).toBe("string");
238
+ expect(f.description.length).toBeGreaterThan(0);
239
+ }
240
+ });
241
+ });
242
+
243
+ // ─── Prompt builders (smoke tests) ────────────────────────────────────────────
244
+
245
+ describe("buildDslGapRefinementPrompt", () => {
246
+ it("includes each gap hint in the output prompt", () => {
247
+ const dsl = makeDsl({ endpoints: [], models: [] });
248
+ const gaps = assessDslRichness(dsl);
249
+ const prompt = buildDslGapRefinementPrompt("# My Spec", gaps);
250
+ for (const gap of gaps) {
251
+ expect(prompt).toContain(gap.hint.slice(0, 30));
252
+ }
253
+ expect(prompt).toContain("# My Spec");
254
+ });
255
+ });
256
+
257
+ describe("buildStructuralAmendmentPrompt", () => {
258
+ it("includes each finding description in the output prompt", () => {
259
+ const findings = [
260
+ { category: "auth_design" as const, description: "POST /users is missing authentication" },
261
+ ];
262
+ const prompt = buildStructuralAmendmentPrompt("# My Spec", findings);
263
+ expect(prompt).toContain("POST /users is missing authentication");
264
+ expect(prompt).toContain("# My Spec");
265
+ });
266
+ });