ai-spec-dev 0.37.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 (67) hide show
  1. package/README.md +381 -1796
  2. package/RELEASE_LOG.md +231 -0
  3. package/cli/commands/create.ts +9 -1176
  4. package/cli/commands/dashboard.ts +1 -1
  5. package/cli/pipeline/helpers.ts +34 -0
  6. package/cli/pipeline/multi-repo.ts +483 -0
  7. package/cli/pipeline/single-repo.ts +755 -0
  8. package/cli/utils.ts +2 -0
  9. package/core/code-generator.ts +52 -341
  10. package/core/codegen/helpers.ts +219 -0
  11. package/core/codegen/topo-sort.ts +98 -0
  12. package/core/constitution-consolidator.ts +2 -2
  13. package/core/dsl-coverage-checker.ts +298 -0
  14. package/core/dsl-extractor.ts +19 -46
  15. package/core/dsl-feedback.ts +1 -1
  16. package/core/dsl-validator.ts +74 -0
  17. package/core/error-feedback.ts +95 -11
  18. package/core/frontend-context-loader.ts +27 -5
  19. package/core/knowledge-memory.ts +52 -0
  20. package/core/mock/fixtures.ts +89 -0
  21. package/core/mock/proxy.ts +380 -0
  22. package/core/mock-server-generator.ts +12 -460
  23. package/core/requirement-decomposer.ts +4 -28
  24. package/core/reviewer.ts +1 -1
  25. package/core/safe-json.ts +76 -0
  26. package/core/spec-updater.ts +5 -21
  27. package/core/token-budget.ts +124 -0
  28. package/core/vcr.ts +20 -1
  29. package/dist/cli/index.js +4110 -3534
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/cli/index.mjs +4237 -3661
  32. package/dist/cli/index.mjs.map +1 -1
  33. package/dist/index.d.mts +18 -16
  34. package/dist/index.d.ts +18 -16
  35. package/dist/index.js +310 -182
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +308 -180
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +2 -2
  40. package/purpose.md +173 -33
  41. package/tests/auto-consolidation.test.ts +109 -0
  42. package/tests/combined-generator.test.ts +81 -0
  43. package/tests/constitution-consolidator.test.ts +161 -0
  44. package/tests/constitution-generator.test.ts +94 -0
  45. package/tests/contract-bridge.test.ts +201 -0
  46. package/tests/design-dialogue.test.ts +108 -0
  47. package/tests/dsl-coverage-checker.test.ts +230 -0
  48. package/tests/dsl-feedback.test.ts +45 -0
  49. package/tests/dsl-validator-xref.test.ts +99 -0
  50. package/tests/error-feedback-repair.test.ts +319 -0
  51. package/tests/error-feedback-validation.test.ts +91 -0
  52. package/tests/frontend-context-loader.test.ts +609 -0
  53. package/tests/global-constitution.test.ts +110 -0
  54. package/tests/key-store.test.ts +73 -0
  55. package/tests/knowledge-memory.test.ts +327 -0
  56. package/tests/project-index.test.ts +206 -0
  57. package/tests/prompt-hasher.test.ts +19 -0
  58. package/tests/requirement-decomposer.test.ts +171 -0
  59. package/tests/reviewer.test.ts +4 -1
  60. package/tests/run-logger.test.ts +289 -0
  61. package/tests/run-snapshot.test.ts +113 -0
  62. package/tests/safe-json.test.ts +63 -0
  63. package/tests/spec-updater.test.ts +161 -0
  64. package/tests/test-generator.test.ts +146 -0
  65. package/tests/token-budget.test.ts +124 -0
  66. package/tests/vcr-hash.test.ts +101 -0
  67. package/tests/workspace-loader.test.ts +277 -0
@@ -0,0 +1,319 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs-extra";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import {
6
+ parseErrors,
7
+ parseRelativeImports,
8
+ buildRepairOrder,
9
+ detectBuildCommand,
10
+ detectTestCommand,
11
+ detectLintCommand,
12
+ ErrorEntry,
13
+ } from "../core/error-feedback";
14
+
15
+ // ─── parseErrors ─────────────────────────────────────────────────────────────
16
+
17
+ describe("parseErrors", () => {
18
+ it("extracts file:line errors from TypeScript output", () => {
19
+ const output = `src/foo.ts:10:5 - error TS2345: Argument of type 'string' is not assignable.\nsrc/bar.ts:20:1 - error TS2322: Type mismatch.`;
20
+ const errors = parseErrors(output, "build");
21
+ expect(errors).toHaveLength(2);
22
+ expect(errors[0].file).toBe("src/foo.ts");
23
+ expect(errors[0].source).toBe("build");
24
+ expect(errors[1].file).toBe("src/bar.ts");
25
+ });
26
+
27
+ it("filters out npm timing and node_modules lines", () => {
28
+ const output = `npm timing idealTree Completed in 100ms\nnode_modules/some-lib/index.js:5 error\nsrc/app.ts:1:1 - error TS1234: oops`;
29
+ const errors = parseErrors(output, "build");
30
+ expect(errors).toHaveLength(1);
31
+ expect(errors[0].file).toBe("src/app.ts");
32
+ });
33
+
34
+ it("filters out stack trace lines", () => {
35
+ const output = `src/x.ts:5:1 - error TS9999: bad\n at Object.<anonymous> (/test)\nNode.js v20.0.0`;
36
+ const errors = parseErrors(output, "test");
37
+ expect(errors).toHaveLength(1);
38
+ });
39
+
40
+ it("returns empty for empty output", () => {
41
+ expect(parseErrors("", "build")).toEqual([]);
42
+ expect(parseErrors(" \n ", "lint")).toEqual([]);
43
+ });
44
+
45
+ it("caps at 20 errors", () => {
46
+ const lines = Array.from({ length: 30 }, (_, i) =>
47
+ `src/file${i}.ts:1:1 - error TS0000: error ${i}`
48
+ ).join("\n");
49
+ const errors = parseErrors(lines, "build");
50
+ expect(errors).toHaveLength(20);
51
+ });
52
+
53
+ it("truncates long error messages at 400 chars", () => {
54
+ const longMsg = "x".repeat(500);
55
+ const output = `src/long.ts:1:1 - ${longMsg}`;
56
+ const errors = parseErrors(output, "build");
57
+ expect(errors[0].message.length).toBeLessThanOrEqual(400);
58
+ });
59
+
60
+ it("handles Go test output", () => {
61
+ const output = `main_test.go:15: expected 1, got 2`;
62
+ const errors = parseErrors(output, "test");
63
+ expect(errors).toHaveLength(1);
64
+ expect(errors[0].file).toBe("main_test.go");
65
+ });
66
+
67
+ it("handles Python test output", () => {
68
+ const output = `test_main.py:42: AssertionError`;
69
+ const errors = parseErrors(output, "test");
70
+ expect(errors).toHaveLength(1);
71
+ expect(errors[0].file).toBe("test_main.py");
72
+ });
73
+
74
+ it("skips summary lines without file references", () => {
75
+ const output = `Found 12 errors.\nWARNING: unstable\nsrc/ok.ts:1:1 - error TS123: real`;
76
+ const errors = parseErrors(output, "build");
77
+ expect(errors).toHaveLength(1);
78
+ });
79
+ });
80
+
81
+ // ─── parseRelativeImports ────────────────────────────────────────────────────
82
+
83
+ describe("parseRelativeImports", () => {
84
+ it("extracts relative imports", () => {
85
+ const content = `import { Foo } from './foo';\nimport Bar from '../bar';`;
86
+ const result = parseRelativeImports(content, "src/index.ts");
87
+ expect(result).toContain("src/foo");
88
+ expect(result).toContain("bar");
89
+ });
90
+
91
+ it("skips absolute/alias imports", () => {
92
+ const content = `import axios from 'axios';\nimport { x } from '@/utils/x';`;
93
+ const result = parseRelativeImports(content, "src/index.ts");
94
+ expect(result).toHaveLength(0);
95
+ });
96
+
97
+ it("skips type-only imports", () => {
98
+ const content = `import type { Foo } from './foo';`;
99
+ const result = parseRelativeImports(content, "src/index.ts");
100
+ expect(result).toHaveLength(0);
101
+ });
102
+
103
+ it("handles multi-line named imports", () => {
104
+ const content = `import {\n Foo,\n Bar\n} from './utils';`;
105
+ const result = parseRelativeImports(content, "src/index.ts");
106
+ expect(result).toContain("src/utils");
107
+ });
108
+
109
+ it("returns empty for no imports", () => {
110
+ expect(parseRelativeImports("const x = 1;", "src/a.ts")).toEqual([]);
111
+ });
112
+ });
113
+
114
+ // ─── buildRepairOrder ────────────────────────────────────────────────────────
115
+
116
+ describe("buildRepairOrder", () => {
117
+ let tmpDir: string;
118
+
119
+ beforeEach(async () => {
120
+ tmpDir = path.join(os.tmpdir(), `repair-test-${Date.now()}`);
121
+ await fs.ensureDir(path.join(tmpDir, "src"));
122
+ });
123
+
124
+ afterEach(async () => {
125
+ await fs.remove(tmpDir);
126
+ });
127
+
128
+ it("returns single file as-is", async () => {
129
+ const map = new Map<string, ErrorEntry[]>([
130
+ ["src/a.ts", [{ source: "build", message: "error", file: "src/a.ts" }]],
131
+ ]);
132
+ const result = await buildRepairOrder(map, tmpDir);
133
+ expect(result).toHaveLength(1);
134
+ expect(result[0][0]).toBe("src/a.ts");
135
+ });
136
+
137
+ it("sorts dependency before dependent", async () => {
138
+ // b.ts imports from a.ts → a.ts should come first
139
+ await fs.writeFile(path.join(tmpDir, "src/a.ts"), "export const x = 1;");
140
+ await fs.writeFile(path.join(tmpDir, "src/b.ts"), "import { x } from './a';");
141
+
142
+ const err = (file: string): ErrorEntry => ({ source: "build", message: "err", file });
143
+ const map = new Map<string, ErrorEntry[]>([
144
+ ["src/b.ts", [err("src/b.ts")]],
145
+ ["src/a.ts", [err("src/a.ts")]],
146
+ ]);
147
+
148
+ const result = await buildRepairOrder(map, tmpDir);
149
+ const order = result.map(([f]) => f);
150
+ expect(order.indexOf("src/a.ts")).toBeLessThan(order.indexOf("src/b.ts"));
151
+ });
152
+
153
+ it("handles unreadable files gracefully", async () => {
154
+ const map = new Map<string, ErrorEntry[]>([
155
+ ["src/missing.ts", [{ source: "build", message: "err", file: "src/missing.ts" }]],
156
+ ["src/other.ts", [{ source: "build", message: "err", file: "src/other.ts" }]],
157
+ ]);
158
+ // Should not throw
159
+ const result = await buildRepairOrder(map, tmpDir);
160
+ expect(result).toHaveLength(2);
161
+ });
162
+
163
+ it("handles circular deps without hanging", async () => {
164
+ await fs.writeFile(path.join(tmpDir, "src/a.ts"), "import { y } from './b'; export const x = 1;");
165
+ await fs.writeFile(path.join(tmpDir, "src/b.ts"), "import { x } from './a'; export const y = 2;");
166
+
167
+ const err = (file: string): ErrorEntry => ({ source: "build", message: "err", file });
168
+ const map = new Map<string, ErrorEntry[]>([
169
+ ["src/a.ts", [err("src/a.ts")]],
170
+ ["src/b.ts", [err("src/b.ts")]],
171
+ ]);
172
+
173
+ const result = await buildRepairOrder(map, tmpDir);
174
+ expect(result).toHaveLength(2);
175
+ });
176
+ });
177
+
178
+ // ─── detect* commands ────────────────────────────────────────────────────────
179
+
180
+ describe("detectBuildCommand", () => {
181
+ let tmpDir: string;
182
+
183
+ beforeEach(async () => {
184
+ tmpDir = path.join(os.tmpdir(), `detect-test-${Date.now()}`);
185
+ await fs.ensureDir(tmpDir);
186
+ });
187
+
188
+ afterEach(async () => {
189
+ await fs.remove(tmpDir);
190
+ });
191
+
192
+ it("returns null for non-TS projects", () => {
193
+ expect(detectBuildCommand(tmpDir)).toBeNull();
194
+ });
195
+
196
+ it("returns tsc for plain TS projects", async () => {
197
+ await fs.writeFile(path.join(tmpDir, "tsconfig.json"), "{}");
198
+ expect(detectBuildCommand(tmpDir)).toBe("npx tsc --noEmit");
199
+ });
200
+
201
+ it("returns vue-tsc for Vue projects", async () => {
202
+ await fs.writeFile(path.join(tmpDir, "tsconfig.json"), "{}");
203
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
204
+ devDependencies: { "vue-tsc": "^1.0.0" },
205
+ });
206
+ expect(detectBuildCommand(tmpDir)).toBe("npx vue-tsc --noEmit");
207
+ });
208
+
209
+ it("prefers npm type-check script if present", async () => {
210
+ await fs.writeFile(path.join(tmpDir, "tsconfig.json"), "{}");
211
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
212
+ scripts: { "type-check": "tsc --noEmit" },
213
+ });
214
+ expect(detectBuildCommand(tmpDir)).toBe("npm run type-check");
215
+ });
216
+ });
217
+
218
+ describe("detectTestCommand", () => {
219
+ let tmpDir: string;
220
+
221
+ beforeEach(async () => {
222
+ tmpDir = path.join(os.tmpdir(), `detect-test-${Date.now()}`);
223
+ await fs.ensureDir(tmpDir);
224
+ });
225
+
226
+ afterEach(async () => {
227
+ await fs.remove(tmpDir);
228
+ });
229
+
230
+ it("returns go test for Go projects", async () => {
231
+ await fs.writeFile(path.join(tmpDir, "go.mod"), "module test");
232
+ expect(detectTestCommand(tmpDir)).toBe("go test ./...");
233
+ });
234
+
235
+ it("returns cargo test for Rust projects", async () => {
236
+ await fs.writeFile(path.join(tmpDir, "Cargo.toml"), "[package]");
237
+ expect(detectTestCommand(tmpDir)).toBe("cargo test");
238
+ });
239
+
240
+ it("returns pytest for Python projects", async () => {
241
+ await fs.writeFile(path.join(tmpDir, "requirements.txt"), "flask");
242
+ expect(detectTestCommand(tmpDir)).toBe("pytest");
243
+ });
244
+
245
+ it("returns npm test when scripts.test exists", async () => {
246
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
247
+ scripts: { test: "vitest run" },
248
+ });
249
+ expect(detectTestCommand(tmpDir)).toBe("npm test");
250
+ });
251
+
252
+ it("detects vitest config file", async () => {
253
+ await fs.writeJson(path.join(tmpDir, "package.json"), { scripts: {} });
254
+ await fs.writeFile(path.join(tmpDir, "vitest.config.ts"), "");
255
+ expect(detectTestCommand(tmpDir)).toBe("npx vitest run");
256
+ });
257
+
258
+ it("returns null when nothing detected", () => {
259
+ expect(detectTestCommand(tmpDir)).toBeNull();
260
+ });
261
+
262
+ it("returns phpunit for PHP projects", async () => {
263
+ await fs.writeFile(path.join(tmpDir, "composer.json"), "{}");
264
+ await fs.ensureDir(path.join(tmpDir, "vendor", "bin"));
265
+ await fs.writeFile(path.join(tmpDir, "vendor", "bin", "phpunit"), "");
266
+ expect(detectTestCommand(tmpDir)).toBe("./vendor/bin/phpunit --colors=never");
267
+ });
268
+
269
+ it("returns mvn test for Maven projects", async () => {
270
+ await fs.writeFile(path.join(tmpDir, "pom.xml"), "<project/>");
271
+ expect(detectTestCommand(tmpDir)).toBe("mvn test -q");
272
+ });
273
+ });
274
+
275
+ describe("detectLintCommand", () => {
276
+ let tmpDir: string;
277
+
278
+ beforeEach(async () => {
279
+ tmpDir = path.join(os.tmpdir(), `detect-lint-${Date.now()}`);
280
+ await fs.ensureDir(tmpDir);
281
+ });
282
+
283
+ afterEach(async () => {
284
+ await fs.remove(tmpDir);
285
+ });
286
+
287
+ it("returns go vet for Go projects", async () => {
288
+ await fs.writeFile(path.join(tmpDir, "go.mod"), "module test");
289
+ expect(detectLintCommand(tmpDir)).toBe("go vet ./...");
290
+ });
291
+
292
+ it("returns cargo clippy for Rust projects", async () => {
293
+ await fs.writeFile(path.join(tmpDir, "Cargo.toml"), "[package]");
294
+ expect(detectLintCommand(tmpDir)).toBe("cargo clippy -- -D warnings");
295
+ });
296
+
297
+ it("returns npm run lint when scripts.lint exists", async () => {
298
+ await fs.writeJson(path.join(tmpDir, "package.json"), {
299
+ scripts: { lint: "eslint ." },
300
+ });
301
+ expect(detectLintCommand(tmpDir)).toBe("npm run lint");
302
+ });
303
+
304
+ it("detects eslint config files", async () => {
305
+ await fs.writeJson(path.join(tmpDir, "package.json"), { scripts: {} });
306
+ await fs.writeFile(path.join(tmpDir, ".eslintrc.js"), "");
307
+ expect(detectLintCommand(tmpDir)).toBe("npx eslint . --max-warnings=0");
308
+ });
309
+
310
+ it("returns null for Java/Maven projects", async () => {
311
+ await fs.writeFile(path.join(tmpDir, "pom.xml"), "<project/>");
312
+ expect(detectLintCommand(tmpDir)).toBeNull();
313
+ });
314
+
315
+ it("returns ruff/flake8 for Python projects", async () => {
316
+ await fs.writeFile(path.join(tmpDir, "pyproject.toml"), "");
317
+ expect(detectLintCommand(tmpDir)).toBe("ruff check . || flake8 .");
318
+ });
319
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs-extra";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { validateTestFilesExist } from "../core/error-feedback";
6
+
7
+ describe("validateTestFilesExist", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = path.join(os.tmpdir(), `ef-test-${Date.now()}`);
12
+ await fs.ensureDir(tmpDir);
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.remove(tmpDir);
17
+ });
18
+
19
+ it("returns invalid when no test files provided", () => {
20
+ const result = validateTestFilesExist(tmpDir, []);
21
+ expect(result.valid).toBe(false);
22
+ expect(result.fileCount).toBe(0);
23
+ expect(result.reason).toContain("No test files");
24
+ });
25
+
26
+ it("returns invalid when test files do not exist on disk", () => {
27
+ const result = validateTestFilesExist(tmpDir, ["tests/nonexistent.test.ts"]);
28
+ expect(result.valid).toBe(false);
29
+ });
30
+
31
+ it("returns invalid when files exist but contain no test patterns", () => {
32
+ const testFile = path.join(tmpDir, "empty.test.ts");
33
+ fs.writeFileSync(testFile, "// just a comment\nexport const x = 1;\n");
34
+ const result = validateTestFilesExist(tmpDir, ["empty.test.ts"]);
35
+ expect(result.valid).toBe(false);
36
+ expect(result.reason).toContain("none contain actual test cases");
37
+ });
38
+
39
+ it("returns valid when files contain describe()", () => {
40
+ const testFile = path.join(tmpDir, "valid.test.ts");
41
+ fs.writeFileSync(testFile, 'import { describe } from "vitest";\ndescribe("Foo", () => {});\n');
42
+ const result = validateTestFilesExist(tmpDir, ["valid.test.ts"]);
43
+ expect(result.valid).toBe(true);
44
+ expect(result.fileCount).toBe(1);
45
+ });
46
+
47
+ it("returns valid when files contain test()", () => {
48
+ const testFile = path.join(tmpDir, "valid.test.ts");
49
+ fs.writeFileSync(testFile, 'test("should work", () => { expect(1).toBe(1); });\n');
50
+ const result = validateTestFilesExist(tmpDir, ["valid.test.ts"]);
51
+ expect(result.valid).toBe(true);
52
+ });
53
+
54
+ it("returns valid when files contain Go test func", () => {
55
+ const testFile = path.join(tmpDir, "main_test.go");
56
+ fs.writeFileSync(testFile, 'func TestMain(t *testing.T) {\n t.Log("ok")\n}\n');
57
+ const result = validateTestFilesExist(tmpDir, ["main_test.go"]);
58
+ expect(result.valid).toBe(true);
59
+ });
60
+
61
+ it("returns valid when files contain Python test", () => {
62
+ const testFile = path.join(tmpDir, "test_main.py");
63
+ fs.writeFileSync(testFile, 'def test_something():\n assert True\n');
64
+ const result = validateTestFilesExist(tmpDir, ["test_main.py"]);
65
+ expect(result.valid).toBe(true);
66
+ });
67
+
68
+ it("counts only valid files", () => {
69
+ const validFile = path.join(tmpDir, "valid.test.ts");
70
+ const emptyFile = path.join(tmpDir, "empty.test.ts");
71
+ fs.writeFileSync(validFile, 'describe("X", () => {});\n');
72
+ fs.writeFileSync(emptyFile, "// no tests\n");
73
+ const result = validateTestFilesExist(tmpDir, ["valid.test.ts", "empty.test.ts"]);
74
+ expect(result.valid).toBe(true);
75
+ expect(result.fileCount).toBe(1);
76
+ });
77
+
78
+ it("handles Java @Test annotation", () => {
79
+ const testFile = path.join(tmpDir, "TestFoo.java");
80
+ fs.writeFileSync(testFile, 'public class TestFoo {\n @Test\n public void testBar() {}\n}\n');
81
+ const result = validateTestFilesExist(tmpDir, ["TestFoo.java"]);
82
+ expect(result.valid).toBe(true);
83
+ });
84
+
85
+ it("handles Rust #[test] attribute", () => {
86
+ const testFile = path.join(tmpDir, "test_main.rs");
87
+ fs.writeFileSync(testFile, '#[test]\nfn test_foo() {\n assert!(true);\n}\n');
88
+ const result = validateTestFilesExist(tmpDir, ["test_main.rs"]);
89
+ expect(result.valid).toBe(true);
90
+ });
91
+ });