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.
- package/.claude/settings.local.json +18 -0
- package/README.md +1211 -146
- package/RELEASE_LOG.md +1444 -0
- package/cli/index.ts +1961 -0
- package/cli/welcome.ts +151 -0
- package/core/code-generator.ts +740 -0
- package/core/combined-generator.ts +63 -0
- package/core/constitution-consolidator.ts +141 -0
- package/core/constitution-generator.ts +89 -0
- package/core/context-loader.ts +453 -0
- package/core/contract-bridge.ts +217 -0
- package/core/dsl-extractor.ts +337 -0
- package/core/dsl-types.ts +166 -0
- package/core/dsl-validator.ts +450 -0
- package/core/error-feedback.ts +354 -0
- package/core/frontend-context-loader.ts +602 -0
- package/core/global-constitution.ts +88 -0
- package/core/key-store.ts +49 -0
- package/core/knowledge-memory.ts +171 -0
- package/core/mock-server-generator.ts +571 -0
- package/core/openapi-exporter.ts +361 -0
- package/core/requirement-decomposer.ts +198 -0
- package/core/reviewer.ts +259 -0
- package/core/spec-assessor.ts +99 -0
- package/core/spec-generator.ts +428 -0
- package/core/spec-refiner.ts +89 -0
- package/core/spec-updater.ts +227 -0
- package/core/spec-versioning.ts +213 -0
- package/core/task-generator.ts +174 -0
- package/core/test-generator.ts +273 -0
- package/core/workspace-loader.ts +256 -0
- package/dist/cli/index.js +6717 -672
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6717 -670
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +147 -27
- package/dist/index.d.ts +147 -27
- package/dist/index.js +2337 -286
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2329 -285
- package/dist/index.mjs.map +1 -1
- package/git/worktree.ts +109 -0
- package/index.ts +9 -0
- package/package.json +4 -28
- package/prompts/codegen.prompt.ts +259 -0
- package/prompts/consolidate.prompt.ts +73 -0
- package/prompts/constitution.prompt.ts +63 -0
- package/prompts/decompose.prompt.ts +168 -0
- package/prompts/dsl.prompt.ts +203 -0
- package/prompts/frontend-spec.prompt.ts +191 -0
- package/prompts/global-constitution.prompt.ts +61 -0
- package/prompts/spec-assess.prompt.ts +53 -0
- package/prompts/spec.prompt.ts +102 -0
- package/prompts/tasks.prompt.ts +35 -0
- package/prompts/testgen.prompt.ts +84 -0
- package/prompts/update.prompt.ts +131 -0
- package/purpose.docx +0 -0
- package/purpose.md +444 -0
- package/tsconfig.json +14 -0
- 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
|
+
}
|