ai-spec-dev 0.31.0 → 0.35.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/commands/add-lesson.md +34 -0
- package/.claude/commands/check-layers.md +65 -0
- package/.claude/commands/installed-deps.md +35 -0
- package/.claude/commands/recall-lessons.md +40 -0
- package/.claude/commands/scan-singletons.md +45 -0
- package/.claude/commands/verify-imports.md +48 -0
- package/.claude/settings.local.json +15 -1
- package/README.md +531 -213
- package/RELEASE_LOG.md +460 -0
- package/cli/commands/config.ts +93 -0
- package/cli/commands/create.ts +1233 -0
- package/cli/commands/dashboard.ts +62 -0
- package/cli/commands/export.ts +66 -0
- package/cli/commands/init.ts +190 -0
- package/cli/commands/learn.ts +30 -0
- package/cli/commands/logs.ts +106 -0
- package/cli/commands/mock.ts +175 -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/scan.ts +99 -0
- package/cli/commands/trend.ts +36 -0
- package/cli/commands/types.ts +69 -0
- package/cli/commands/update.ts +178 -0
- package/cli/commands/vcr.ts +70 -0
- package/cli/commands/workspace.ts +219 -0
- package/cli/index.ts +34 -2240
- package/cli/utils.ts +83 -0
- package/core/combined-generator.ts +13 -3
- package/core/dashboard-generator.ts +340 -0
- package/core/design-dialogue.ts +124 -0
- package/core/dsl-feedback.ts +285 -0
- package/core/error-feedback.ts +46 -2
- package/core/project-index.ts +301 -0
- package/core/reviewer.ts +84 -6
- package/core/run-logger.ts +109 -3
- package/core/run-trend.ts +261 -0
- package/core/self-evaluator.ts +139 -7
- package/core/spec-generator.ts +14 -8
- package/core/task-generator.ts +17 -0
- package/core/types-generator.ts +219 -0
- package/core/vcr.ts +210 -0
- package/dist/cli/index.js +6692 -4512
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6692 -4512
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +420 -224
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +418 -224
- package/dist/index.mjs.map +1 -1
- package/docs-assets/purpose/architecture-overview.svg +64 -0
- package/docs-assets/purpose/create-pipeline.svg +113 -0
- package/docs-assets/purpose/task-layering.svg +74 -0
- package/package.json +6 -3
- package/prompts/codegen.prompt.ts +97 -9
- package/prompts/design.prompt.ts +59 -0
- package/prompts/spec.prompt.ts +8 -1
- package/prompts/tasks.prompt.ts +27 -2
- package/purpose.md +600 -174
- 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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildTaskPrompt,
|
|
4
|
+
TaskGenerator,
|
|
5
|
+
SpecTask,
|
|
6
|
+
printTasks,
|
|
7
|
+
} from "../core/task-generator";
|
|
8
|
+
import type { AIProvider } from "../core/spec-generator";
|
|
9
|
+
import type { ProjectContext } from "../core/context-loader";
|
|
10
|
+
|
|
11
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const TASK_DATA_LAYER: SpecTask = {
|
|
14
|
+
id: "T-001",
|
|
15
|
+
title: "Create User model",
|
|
16
|
+
description: "Define Prisma schema for User",
|
|
17
|
+
layer: "data",
|
|
18
|
+
filesToTouch: ["prisma/schema.prisma"],
|
|
19
|
+
acceptanceCriteria: ["User model has email and passwordHash fields"],
|
|
20
|
+
dependencies: [],
|
|
21
|
+
priority: "high",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const TASK_SERVICE_LAYER: SpecTask = {
|
|
25
|
+
id: "T-002",
|
|
26
|
+
title: "Implement AuthService",
|
|
27
|
+
description: "Handle login logic",
|
|
28
|
+
layer: "service",
|
|
29
|
+
filesToTouch: ["src/services/auth.service.ts"],
|
|
30
|
+
acceptanceCriteria: ["Returns JWT on success", "Throws on invalid credentials"],
|
|
31
|
+
dependencies: ["T-001"],
|
|
32
|
+
priority: "high",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const TASK_API_LAYER: SpecTask = {
|
|
36
|
+
id: "T-003",
|
|
37
|
+
title: "Add login endpoint",
|
|
38
|
+
description: "POST /api/auth/login",
|
|
39
|
+
layer: "api",
|
|
40
|
+
filesToTouch: ["src/api/auth.controller.ts"],
|
|
41
|
+
acceptanceCriteria: ["Returns 200 with token"],
|
|
42
|
+
dependencies: ["T-002"],
|
|
43
|
+
priority: "medium",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const TASK_TEST_LAYER: SpecTask = {
|
|
47
|
+
id: "T-004",
|
|
48
|
+
title: "Write auth tests",
|
|
49
|
+
description: "Test AuthService",
|
|
50
|
+
layer: "test",
|
|
51
|
+
filesToTouch: ["tests/auth.service.test.ts"],
|
|
52
|
+
acceptanceCriteria: ["All happy paths covered"],
|
|
53
|
+
dependencies: ["T-002"],
|
|
54
|
+
priority: "low",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const MINIMAL_CONTEXT: ProjectContext = {
|
|
58
|
+
techStack: ["Node.js", "TypeScript", "Express"],
|
|
59
|
+
fileStructure: ["src/services/user.service.ts", "src/api/user.controller.ts"],
|
|
60
|
+
apiStructure: ["src/api/user.controller.ts"],
|
|
61
|
+
constitution: "Use camelCase for all identifiers.",
|
|
62
|
+
sharedConfigFiles: [],
|
|
63
|
+
dependencies: {},
|
|
64
|
+
errorPatterns: [],
|
|
65
|
+
projectType: "backend",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function makeProvider(response: string): AIProvider {
|
|
69
|
+
return { generate: vi.fn().mockResolvedValue(response) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── buildTaskPrompt ──────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
describe("buildTaskPrompt", () => {
|
|
75
|
+
it("returns spec unchanged when no context provided", () => {
|
|
76
|
+
const result = buildTaskPrompt("my spec");
|
|
77
|
+
expect(result).toBe("my spec");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("includes spec content", () => {
|
|
81
|
+
const result = buildTaskPrompt("MY SPEC", MINIMAL_CONTEXT);
|
|
82
|
+
expect(result).toContain("MY SPEC");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("includes constitution when present", () => {
|
|
86
|
+
const result = buildTaskPrompt("spec", MINIMAL_CONTEXT);
|
|
87
|
+
expect(result).toContain("Use camelCase for all identifiers.");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("includes tech stack", () => {
|
|
91
|
+
const result = buildTaskPrompt("spec", MINIMAL_CONTEXT);
|
|
92
|
+
expect(result).toContain("Node.js");
|
|
93
|
+
expect(result).toContain("TypeScript");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("includes file structure entries", () => {
|
|
97
|
+
const result = buildTaskPrompt("spec", MINIMAL_CONTEXT);
|
|
98
|
+
expect(result).toContain("src/services/user.service.ts");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("includes API structure entries", () => {
|
|
102
|
+
const result = buildTaskPrompt("spec", MINIMAL_CONTEXT);
|
|
103
|
+
expect(result).toContain("src/api/user.controller.ts");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("omits constitution section when constitution is empty", () => {
|
|
107
|
+
const ctx = { ...MINIMAL_CONTEXT, constitution: "" };
|
|
108
|
+
const result = buildTaskPrompt("spec", ctx);
|
|
109
|
+
expect(result).not.toContain("Project Constitution");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("includes shared config files when present", () => {
|
|
113
|
+
const ctx: ProjectContext = {
|
|
114
|
+
...MINIMAL_CONTEXT,
|
|
115
|
+
sharedConfigFiles: [{ path: "src/config/index.ts", category: "config" }],
|
|
116
|
+
};
|
|
117
|
+
const result = buildTaskPrompt("spec", ctx);
|
|
118
|
+
expect(result).toContain("src/config/index.ts");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── TaskGenerator.sortByLayer ────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
describe("TaskGenerator.sortByLayer", () => {
|
|
125
|
+
const provider = makeProvider("[]");
|
|
126
|
+
const gen = new TaskGenerator(provider);
|
|
127
|
+
|
|
128
|
+
it("sorts data before service before api before test", () => {
|
|
129
|
+
const tasks = [TASK_TEST_LAYER, TASK_API_LAYER, TASK_SERVICE_LAYER, TASK_DATA_LAYER];
|
|
130
|
+
const sorted = gen.sortByLayer(tasks);
|
|
131
|
+
const layers = sorted.map((t) => t.layer);
|
|
132
|
+
expect(layers).toEqual(["data", "service", "api", "test"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("does not mutate the original array", () => {
|
|
136
|
+
const tasks = [TASK_SERVICE_LAYER, TASK_DATA_LAYER];
|
|
137
|
+
const original = [...tasks];
|
|
138
|
+
gen.sortByLayer(tasks);
|
|
139
|
+
expect(tasks.map((t) => t.id)).toEqual(original.map((t) => t.id));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("maintains stable order within the same layer (by id)", () => {
|
|
143
|
+
const t1: SpecTask = { ...TASK_SERVICE_LAYER, id: "T-001" };
|
|
144
|
+
const t2: SpecTask = { ...TASK_SERVICE_LAYER, id: "T-002" };
|
|
145
|
+
const sorted = gen.sortByLayer([t2, t1]);
|
|
146
|
+
expect(sorted[0].id).toBe("T-001");
|
|
147
|
+
expect(sorted[1].id).toBe("T-002");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("handles an empty array", () => {
|
|
151
|
+
expect(gen.sortByLayer([])).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("handles a single task", () => {
|
|
155
|
+
expect(gen.sortByLayer([TASK_API_LAYER])).toEqual([TASK_API_LAYER]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("covers all 7 layers in the correct order", () => {
|
|
159
|
+
const tasks: SpecTask[] = (["test", "route", "view", "api", "service", "infra", "data"] as const).map(
|
|
160
|
+
(layer, i) => ({ ...TASK_DATA_LAYER, id: `T-${i}`, layer })
|
|
161
|
+
);
|
|
162
|
+
const sorted = gen.sortByLayer(tasks);
|
|
163
|
+
expect(sorted.map((t) => t.layer)).toEqual([
|
|
164
|
+
"data", "infra", "service", "api", "view", "route", "test",
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ─── TaskGenerator.generateTasks — task parsing ───────────────────────────────
|
|
170
|
+
|
|
171
|
+
describe("TaskGenerator.generateTasks — task parsing", () => {
|
|
172
|
+
it("parses a bare JSON array from provider output", async () => {
|
|
173
|
+
const tasks = [TASK_DATA_LAYER, TASK_SERVICE_LAYER];
|
|
174
|
+
const provider = makeProvider(JSON.stringify(tasks));
|
|
175
|
+
const gen = new TaskGenerator(provider);
|
|
176
|
+
const result = await gen.generateTasks("spec");
|
|
177
|
+
expect(result).toHaveLength(2);
|
|
178
|
+
expect(result[0].id).toBe("T-001");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("parses a JSON array wrapped in a code fence", async () => {
|
|
182
|
+
const tasks = [TASK_API_LAYER];
|
|
183
|
+
const fenced = "```json\n" + JSON.stringify(tasks) + "\n```";
|
|
184
|
+
const provider = makeProvider(fenced);
|
|
185
|
+
const gen = new TaskGenerator(provider);
|
|
186
|
+
const result = await gen.generateTasks("spec");
|
|
187
|
+
expect(result).toHaveLength(1);
|
|
188
|
+
expect(result[0].layer).toBe("api");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns empty array when provider returns invalid JSON", async () => {
|
|
192
|
+
const provider = makeProvider("I cannot generate tasks right now.");
|
|
193
|
+
const gen = new TaskGenerator(provider);
|
|
194
|
+
const result = await gen.generateTasks("spec");
|
|
195
|
+
expect(result).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns empty array when provider returns a JSON object (not array)", async () => {
|
|
199
|
+
const provider = makeProvider(JSON.stringify({ tasks: [] }));
|
|
200
|
+
const gen = new TaskGenerator(provider);
|
|
201
|
+
const result = await gen.generateTasks("spec");
|
|
202
|
+
expect(result).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("passes context to the prompt when provided", async () => {
|
|
206
|
+
const provider = makeProvider("[]");
|
|
207
|
+
const gen = new TaskGenerator(provider);
|
|
208
|
+
await gen.generateTasks("spec", MINIMAL_CONTEXT);
|
|
209
|
+
const [prompt] = (provider.generate as ReturnType<typeof vi.fn>).mock.calls[0];
|
|
210
|
+
expect(prompt).toContain("Node.js");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ─── printTasks ───────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
describe("printTasks", () => {
|
|
217
|
+
it("runs without throwing for a mixed task list", () => {
|
|
218
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
219
|
+
expect(() =>
|
|
220
|
+
printTasks([TASK_DATA_LAYER, TASK_SERVICE_LAYER, TASK_API_LAYER, TASK_TEST_LAYER])
|
|
221
|
+
).not.toThrow();
|
|
222
|
+
spy.mockRestore();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("runs without throwing for an empty task list", () => {
|
|
226
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
227
|
+
expect(() => printTasks([])).not.toThrow();
|
|
228
|
+
spy.mockRestore();
|
|
229
|
+
});
|
|
230
|
+
});
|