ai-spec-dev 0.1.0 → 0.14.1

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 (60) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/README.md +1211 -146
  3. package/RELEASE_LOG.md +1444 -0
  4. package/cli/index.ts +1961 -0
  5. package/cli/welcome.ts +151 -0
  6. package/core/code-generator.ts +740 -0
  7. package/core/combined-generator.ts +63 -0
  8. package/core/constitution-consolidator.ts +141 -0
  9. package/core/constitution-generator.ts +89 -0
  10. package/core/context-loader.ts +453 -0
  11. package/core/contract-bridge.ts +217 -0
  12. package/core/dsl-extractor.ts +337 -0
  13. package/core/dsl-types.ts +166 -0
  14. package/core/dsl-validator.ts +450 -0
  15. package/core/error-feedback.ts +354 -0
  16. package/core/frontend-context-loader.ts +602 -0
  17. package/core/global-constitution.ts +88 -0
  18. package/core/key-store.ts +49 -0
  19. package/core/knowledge-memory.ts +171 -0
  20. package/core/mock-server-generator.ts +571 -0
  21. package/core/openapi-exporter.ts +361 -0
  22. package/core/requirement-decomposer.ts +198 -0
  23. package/core/reviewer.ts +259 -0
  24. package/core/spec-assessor.ts +99 -0
  25. package/core/spec-generator.ts +428 -0
  26. package/core/spec-refiner.ts +89 -0
  27. package/core/spec-updater.ts +227 -0
  28. package/core/spec-versioning.ts +213 -0
  29. package/core/task-generator.ts +174 -0
  30. package/core/test-generator.ts +273 -0
  31. package/core/workspace-loader.ts +256 -0
  32. package/dist/cli/index.js +6717 -672
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/index.mjs +6717 -670
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/index.d.mts +147 -27
  37. package/dist/index.d.ts +147 -27
  38. package/dist/index.js +2337 -286
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +2329 -285
  41. package/dist/index.mjs.map +1 -1
  42. package/git/worktree.ts +109 -0
  43. package/index.ts +9 -0
  44. package/package.json +4 -28
  45. package/prompts/codegen.prompt.ts +259 -0
  46. package/prompts/consolidate.prompt.ts +73 -0
  47. package/prompts/constitution.prompt.ts +63 -0
  48. package/prompts/decompose.prompt.ts +168 -0
  49. package/prompts/dsl.prompt.ts +203 -0
  50. package/prompts/frontend-spec.prompt.ts +191 -0
  51. package/prompts/global-constitution.prompt.ts +61 -0
  52. package/prompts/spec-assess.prompt.ts +53 -0
  53. package/prompts/spec.prompt.ts +102 -0
  54. package/prompts/tasks.prompt.ts +35 -0
  55. package/prompts/testgen.prompt.ts +84 -0
  56. package/prompts/update.prompt.ts +131 -0
  57. package/purpose.docx +0 -0
  58. package/purpose.md +444 -0
  59. package/tsconfig.json +14 -0
  60. package/tsup.config.ts +10 -0
@@ -0,0 +1,354 @@
1
+ import chalk from "chalk";
2
+ import { execSync } from "child_process";
3
+ import * as fs from "fs-extra";
4
+ import * as path from "path";
5
+ import { AIProvider } from "./spec-generator";
6
+ import { getCodeGenSystemPrompt } from "../prompts/codegen.prompt";
7
+ import { SpecDSL } from "./dsl-types";
8
+ import { buildDslContextSection } from "./dsl-extractor";
9
+
10
+ // ─── Types ──────────────────────────────────────────────────────────────────────
11
+
12
+ interface ErrorEntry {
13
+ source: "test" | "lint" | "build";
14
+ message: string;
15
+ file?: string;
16
+ }
17
+
18
+ interface FixResult {
19
+ fixed: boolean;
20
+ file: string;
21
+ explanation: string;
22
+ }
23
+
24
+ // ─── Error Detection ────────────────────────────────────────────────────────────
25
+
26
+ function runCommand(cmd: string, cwd: string): { success: boolean; output: string } {
27
+ try {
28
+ const output = execSync(cmd, { cwd, encoding: "utf-8", timeout: 60_000 });
29
+ return { success: true, output };
30
+ } catch (err) {
31
+ const e = err as { stdout?: string; stderr?: string; message?: string };
32
+ return { success: false, output: e.stdout || e.stderr || e.message || "" };
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Detect TypeScript type-check command for the given directory.
38
+ * Returns null for non-TS projects or projects without tsconfig.
39
+ */
40
+ function detectBuildCommand(workingDir: string): string | null {
41
+ // Only applies to Node.js / frontend TypeScript projects
42
+ if (!fs.existsSync(path.join(workingDir, "tsconfig.json"))) return null;
43
+
44
+ // vue-tsc for Vue projects (catches template type errors too)
45
+ const pkgPath = path.join(workingDir, "package.json");
46
+ try {
47
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
48
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
49
+ if (allDeps["vue-tsc"]) return "npx vue-tsc --noEmit";
50
+ // If there's a type-check or tsc script, prefer it
51
+ if (pkg.scripts?.["type-check"]) return "npm run type-check";
52
+ if (pkg.scripts?.["typecheck"]) return "npm run typecheck";
53
+ } catch {
54
+ // ignore
55
+ }
56
+
57
+ return "npx tsc --noEmit";
58
+ }
59
+
60
+ function detectTestCommand(workingDir: string): string | null {
61
+ // ── Go ──────────────────────────────────────────────────────────────────────
62
+ if (fs.existsSync(path.join(workingDir, "go.mod"))) return "go test ./...";
63
+
64
+ // ── PHP (Lumen / Laravel) ───────────────────────────────────────────────────
65
+ if (fs.existsSync(path.join(workingDir, "composer.json"))) {
66
+ return fs.existsSync(path.join(workingDir, "vendor", "bin", "phpunit"))
67
+ ? "./vendor/bin/phpunit --colors=never"
68
+ : "php artisan test --no-ansi";
69
+ }
70
+
71
+ // ── Rust ────────────────────────────────────────────────────────────────────
72
+ if (fs.existsSync(path.join(workingDir, "Cargo.toml"))) return "cargo test";
73
+
74
+ // ── Java (Maven / Gradle) ───────────────────────────────────────────────────
75
+ if (fs.existsSync(path.join(workingDir, "pom.xml"))) return "mvn test -q";
76
+ if (
77
+ fs.existsSync(path.join(workingDir, "build.gradle")) ||
78
+ fs.existsSync(path.join(workingDir, "build.gradle.kts"))
79
+ ) {
80
+ return "./gradlew test";
81
+ }
82
+
83
+ // ── Python ──────────────────────────────────────────────────────────────────
84
+ if (
85
+ fs.existsSync(path.join(workingDir, "requirements.txt")) ||
86
+ fs.existsSync(path.join(workingDir, "pyproject.toml")) ||
87
+ fs.existsSync(path.join(workingDir, "setup.py"))
88
+ ) {
89
+ return "pytest";
90
+ }
91
+
92
+ // ── Node.js ──────────────────────────────────────────────────────────────────
93
+ const pkgPath = path.join(workingDir, "package.json");
94
+ try {
95
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
96
+ if (pkg.scripts?.test) return "npm test";
97
+ if (pkg.scripts?.vitest) return "npx vitest run";
98
+ } catch {
99
+ // no package.json
100
+ }
101
+ for (const f of ["vitest.config.ts", "vitest.config.js", "jest.config.ts", "jest.config.js"]) {
102
+ if (fs.existsSync(path.join(workingDir, f))) {
103
+ return f.startsWith("vitest") ? "npx vitest run" : "npx jest --forceExit";
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ function detectLintCommand(workingDir: string): string | null {
110
+ // ── Go ──────────────────────────────────────────────────────────────────────
111
+ if (fs.existsSync(path.join(workingDir, "go.mod"))) {
112
+ // golangci-lint is optional; fall back to go vet
113
+ return "go vet ./...";
114
+ }
115
+
116
+ // ── PHP (Lumen / Laravel) ───────────────────────────────────────────────────
117
+ if (fs.existsSync(path.join(workingDir, "composer.json"))) {
118
+ return fs.existsSync(path.join(workingDir, "vendor", "bin", "phpstan"))
119
+ ? "./vendor/bin/phpstan analyse --no-progress --memory-limit=512M"
120
+ : null;
121
+ }
122
+
123
+ // ── Rust ────────────────────────────────────────────────────────────────────
124
+ if (fs.existsSync(path.join(workingDir, "Cargo.toml"))) return "cargo clippy -- -D warnings";
125
+
126
+ // ── Java — no universal lint, skip ──────────────────────────────────────────
127
+ if (
128
+ fs.existsSync(path.join(workingDir, "pom.xml")) ||
129
+ fs.existsSync(path.join(workingDir, "build.gradle"))
130
+ ) {
131
+ return null;
132
+ }
133
+
134
+ // ── Python ──────────────────────────────────────────────────────────────────
135
+ if (
136
+ fs.existsSync(path.join(workingDir, "requirements.txt")) ||
137
+ fs.existsSync(path.join(workingDir, "pyproject.toml")) ||
138
+ fs.existsSync(path.join(workingDir, "setup.py"))
139
+ ) {
140
+ // Prefer ruff if available, fall back to flake8
141
+ return "ruff check . || flake8 .";
142
+ }
143
+
144
+ // ── Node.js ──────────────────────────────────────────────────────────────────
145
+ const pkgPath = path.join(workingDir, "package.json");
146
+ try {
147
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
148
+ if (pkg.scripts?.lint) return "npm run lint";
149
+ } catch {
150
+ // ignore
151
+ }
152
+ if (
153
+ fs.existsSync(path.join(workingDir, ".eslintrc")) ||
154
+ fs.existsSync(path.join(workingDir, ".eslintrc.js")) ||
155
+ fs.existsSync(path.join(workingDir, ".eslintrc.json")) ||
156
+ fs.existsSync(path.join(workingDir, "eslint.config.js"))
157
+ ) {
158
+ return "npx eslint . --max-warnings=0";
159
+ }
160
+ return null;
161
+ }
162
+
163
+ function parseErrors(output: string, source: ErrorEntry["source"]): ErrorEntry[] {
164
+ const errors: ErrorEntry[] = [];
165
+ if (!output.trim()) return errors;
166
+
167
+ // Take the last 80 lines — most relevant error output
168
+ const lines = output.split("\n").slice(-80);
169
+
170
+ for (const line of lines) {
171
+ const trimmed = line.trim();
172
+ if (!trimmed) continue;
173
+ // Filter noise: skip npm timing, node warnings, stack traces to node_modules
174
+ if (trimmed.startsWith("npm timing")) continue;
175
+ if (trimmed.includes("node_modules")) continue;
176
+ if (trimmed.startsWith("at ")) continue;
177
+ if (trimmed.startsWith("Node.js ")) continue;
178
+
179
+ // Try to extract file path (supports TS/JS, Go, Python, Java, Rust, PHP)
180
+ const fileMatch = trimmed.match(/^([^:]+\.(?:ts|js|tsx|jsx|go|py|java|rs|php)):\d+/);
181
+ errors.push({
182
+ source,
183
+ message: trimmed.slice(0, 300),
184
+ file: fileMatch?.[1],
185
+ });
186
+ }
187
+
188
+ return errors.slice(0, 20); // cap at 20 errors
189
+ }
190
+
191
+ // ─── Auto-Fix ───────────────────────────────────────────────────────────────────
192
+
193
+ async function attemptFix(
194
+ provider: AIProvider,
195
+ errors: ErrorEntry[],
196
+ workingDir: string,
197
+ dsl?: SpecDSL | null
198
+ ): Promise<FixResult[]> {
199
+ const results: FixResult[] = [];
200
+
201
+ // Group errors by file (fix one file at a time)
202
+ const errorsByFile = new Map<string, ErrorEntry[]>();
203
+ for (const err of errors) {
204
+ const file = err.file || "(unknown)";
205
+ if (!errorsByFile.has(file)) errorsByFile.set(file, []);
206
+ errorsByFile.get(file)!.push(err);
207
+ }
208
+
209
+ for (const [file, fileErrors] of errorsByFile) {
210
+ const fullPath = path.join(workingDir, file);
211
+ let existingContent = "";
212
+ try {
213
+ existingContent = await fs.readFile(fullPath, "utf-8");
214
+ } catch {
215
+ results.push({ fixed: false, file, explanation: "File not found — cannot auto-fix." });
216
+ continue;
217
+ }
218
+
219
+ const dslSection = dsl ? `\n${buildDslContextSection(dsl)}\n` : "";
220
+ const errorSummary = fileErrors.map((e) => `[${e.source}] ${e.message}`).join("\n");
221
+
222
+ const prompt = `Fix the following errors in the file.
223
+
224
+ File: ${file}
225
+ ${dslSection}
226
+ === Errors ===
227
+ ${errorSummary}
228
+
229
+ === Current File Content ===
230
+ ${existingContent}
231
+
232
+ Output ONLY the complete fixed file content. No markdown fences, no explanations.`;
233
+
234
+ try {
235
+ const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
236
+ const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
237
+ await fs.writeFile(fullPath, fixed, "utf-8");
238
+ results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
239
+ console.log(chalk.green(` ✔ Auto-fixed: ${file}`));
240
+ } catch (err) {
241
+ results.push({ fixed: false, file, explanation: `AI fix failed: ${(err as Error).message}` });
242
+ console.log(chalk.yellow(` ⚠ Could not auto-fix: ${file}`));
243
+ }
244
+ }
245
+
246
+ return results;
247
+ }
248
+
249
+ // ─── Public API ─────────────────────────────────────────────────────────────────
250
+
251
+ export interface ErrorFeedbackOptions {
252
+ /** Max fix-verify cycles (default: 2) */
253
+ maxCycles?: number;
254
+ /** Whether to skip test runs (--auto mode may want to skip for speed) */
255
+ skipTests?: boolean;
256
+ /** Whether to skip lint runs */
257
+ skipLint?: boolean;
258
+ /** Whether to skip TypeScript type-check (tsc --noEmit / vue-tsc --noEmit) */
259
+ skipBuild?: boolean;
260
+ }
261
+
262
+ /**
263
+ * Run error feedback loop: detect errors → auto-fix → re-verify.
264
+ * Returns true if all checks pass after fixes, false if errors remain.
265
+ */
266
+ export async function runErrorFeedback(
267
+ provider: AIProvider,
268
+ workingDir: string,
269
+ dsl?: SpecDSL | null,
270
+ opts: ErrorFeedbackOptions = {}
271
+ ): Promise<boolean> {
272
+ const maxCycles = opts.maxCycles ?? 2;
273
+
274
+ console.log(chalk.blue("\n─── Error Feedback ──────────────────────────────"));
275
+
276
+ const testCmd = opts.skipTests ? null : detectTestCommand(workingDir);
277
+ const lintCmd = opts.skipLint ? null : detectLintCommand(workingDir);
278
+ const buildCmd = opts.skipBuild ? null : detectBuildCommand(workingDir);
279
+
280
+ if (!testCmd && !lintCmd && !buildCmd) {
281
+ console.log(chalk.gray(" No test / lint / type-check commands detected. Skipping error feedback."));
282
+ return true;
283
+ }
284
+
285
+ if (buildCmd) console.log(chalk.gray(` Type-check: ${buildCmd}`));
286
+
287
+ for (let cycle = 1; cycle <= maxCycles; cycle++) {
288
+ const allErrors: ErrorEntry[] = [];
289
+
290
+ // ── TypeScript type-check (fast, runs before tests) ──────────────────────
291
+ if (buildCmd) {
292
+ console.log(chalk.gray(`\n [cycle ${cycle}/${maxCycles}] Type-check: ${buildCmd}`));
293
+ const buildResult = runCommand(buildCmd, workingDir);
294
+ if (!buildResult.success) {
295
+ // Detect tool crash (ReferenceError / TypeError inside node_modules) —
296
+ // this means the type-check tool itself is broken (e.g. vue-tsc version
297
+ // incompatibility), NOT a user code error. Skip silently instead of
298
+ // feeding garbage into the auto-fix loop.
299
+ const isToolCrash =
300
+ /ReferenceError:|TypeError:|SyntaxError:/.test(buildResult.output) &&
301
+ buildResult.output.includes("node_modules");
302
+ if (isToolCrash) {
303
+ console.log(chalk.yellow(` ⚠ Type-check tool crashed (possible version incompatibility). Skipping.`));
304
+ console.log(chalk.gray(` Tip: run \`${buildCmd}\` manually to investigate.`));
305
+ } else {
306
+ const buildErrors = parseErrors(buildResult.output, "build");
307
+ allErrors.push(...buildErrors);
308
+ console.log(chalk.yellow(` ✘ Type errors (${buildErrors.length} captured)`));
309
+ }
310
+ } else {
311
+ console.log(chalk.green(" ✔ Type-check passed."));
312
+ }
313
+ }
314
+
315
+ // Run tests
316
+ if (testCmd) {
317
+ console.log(chalk.gray(`\n [cycle ${cycle}/${maxCycles}] Running tests: ${testCmd}`));
318
+ const testResult = runCommand(testCmd, workingDir);
319
+ if (!testResult.success) {
320
+ const testErrors = parseErrors(testResult.output, "test");
321
+ allErrors.push(...testErrors);
322
+ console.log(chalk.yellow(` ✘ Tests failed (${testErrors.length} error(s) captured)`));
323
+ } else {
324
+ console.log(chalk.green(" ✔ Tests passed."));
325
+ }
326
+ }
327
+
328
+ // Run lint
329
+ if (lintCmd) {
330
+ console.log(chalk.gray(` [cycle ${cycle}/${maxCycles}] Running lint: ${lintCmd}`));
331
+ const lintResult = runCommand(lintCmd, workingDir);
332
+ if (!lintResult.success) {
333
+ const lintErrors = parseErrors(lintResult.output, "lint");
334
+ allErrors.push(...lintErrors);
335
+ console.log(chalk.yellow(` ✘ Lint failed (${lintErrors.length} error(s) captured)`));
336
+ } else {
337
+ console.log(chalk.green(" ✔ Lint passed."));
338
+ }
339
+ }
340
+
341
+ if (allErrors.length === 0) {
342
+ console.log(chalk.green(`\n ✔ All checks passed after ${cycle} cycle(s).`));
343
+ return true;
344
+ }
345
+
346
+ if (cycle < maxCycles) {
347
+ console.log(chalk.cyan(`\n Attempting auto-fix (${allErrors.length} error(s))...`));
348
+ await attemptFix(provider, allErrors, workingDir, dsl);
349
+ }
350
+ }
351
+
352
+ console.log(chalk.yellow("\n ⚠ Some errors remain after auto-fix cycles. Manual intervention needed."));
353
+ return false;
354
+ }