ai-spec-dev 0.1.0 → 0.17.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/settings.local.json +18 -0
- package/README.md +1215 -146
- package/RELEASE_LOG.md +1489 -0
- package/cli/index.ts +1981 -0
- package/cli/welcome.ts +151 -0
- package/core/code-generator.ts +757 -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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* combined-generator.ts
|
|
3
|
+
*
|
|
4
|
+
* Generates Spec + Tasks in a single AI call instead of two sequential calls.
|
|
5
|
+
* Avoids the circular-dependency that would arise if spec-generator.ts imported
|
|
6
|
+
* from task-generator.ts (which already imports AIProvider from spec-generator).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AIProvider } from "./spec-generator";
|
|
10
|
+
import { ProjectContext } from "./context-loader";
|
|
11
|
+
import { SpecTask, buildTaskPrompt } from "./task-generator";
|
|
12
|
+
import { specPrompt } from "../prompts/spec.prompt";
|
|
13
|
+
|
|
14
|
+
const TASKS_SEPARATOR = "---TASKS_JSON---";
|
|
15
|
+
|
|
16
|
+
// Appended to specPrompt so the AI outputs spec + tasks in one response.
|
|
17
|
+
const tasksInstruction = `
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
After outputting the complete spec above, append EXACTLY this line on its own (no extra text before or after it):
|
|
21
|
+
${TASKS_SEPARATOR}
|
|
22
|
+
Then output a valid JSON array of implementation tasks. Each element must have these exact fields:
|
|
23
|
+
{"id":"TASK-001","title":"...","description":"1-2 sentences, specific","layer":"data|infra|service|api|test","filesToTouch":["src/..."],"acceptanceCriteria":["verifiable condition"],"dependencies":[],"priority":"high|medium|low"}
|
|
24
|
+
Layer order: data → infra → service → api → test. 4-10 tasks total. filesToTouch must use real paths from the project context.`;
|
|
25
|
+
|
|
26
|
+
export async function generateSpecWithTasks(
|
|
27
|
+
provider: AIProvider,
|
|
28
|
+
idea: string,
|
|
29
|
+
context?: ProjectContext
|
|
30
|
+
): Promise<{ spec: string; tasks: SpecTask[] }> {
|
|
31
|
+
// Use buildTaskPrompt to get the full verified-inventory context,
|
|
32
|
+
// then prepend the idea so the spec generator also sees it.
|
|
33
|
+
const contextBlock = buildTaskPrompt("", context).trim();
|
|
34
|
+
const fullPrompt = [idea, contextBlock].filter(Boolean).join("\n\n");
|
|
35
|
+
|
|
36
|
+
const combinedSystemPrompt = specPrompt + tasksInstruction;
|
|
37
|
+
const raw = await provider.generate(fullPrompt, combinedSystemPrompt);
|
|
38
|
+
return parseSpecAndTasks(raw);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseSpecAndTasks(raw: string): { spec: string; tasks: SpecTask[] } {
|
|
42
|
+
const sepIdx = raw.indexOf(TASKS_SEPARATOR);
|
|
43
|
+
|
|
44
|
+
if (sepIdx === -1) {
|
|
45
|
+
// AI didn't output the separator — return full response as spec, no tasks
|
|
46
|
+
return { spec: raw.trim(), tasks: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const spec = raw.slice(0, sepIdx).trim();
|
|
50
|
+
const tasksRaw = raw.slice(sepIdx + TASKS_SEPARATOR.length).trim();
|
|
51
|
+
|
|
52
|
+
let tasks: SpecTask[] = [];
|
|
53
|
+
try {
|
|
54
|
+
const jsonMatch = tasksRaw.match(/\[[\s\S]*\]/);
|
|
55
|
+
if (jsonMatch) {
|
|
56
|
+
tasks = JSON.parse(jsonMatch[0]);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Parse failed, return empty tasks — caller handles gracefully
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { spec, tasks };
|
|
63
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as fs from "fs-extra";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { AIProvider } from "./spec-generator";
|
|
5
|
+
import { CONSTITUTION_FILE } from "./constitution-generator";
|
|
6
|
+
import {
|
|
7
|
+
consolidateSystemPrompt,
|
|
8
|
+
buildConsolidatePrompt,
|
|
9
|
+
parseConstitutionStats,
|
|
10
|
+
ConstitutionStats,
|
|
11
|
+
} from "../prompts/consolidate.prompt";
|
|
12
|
+
import { computeDiff, printDiff, printDiffSummary } from "./spec-versioning";
|
|
13
|
+
|
|
14
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface ConsolidateOptions {
|
|
17
|
+
/** Preview the result without writing to disk */
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
/** Skip interactive confirmation */
|
|
20
|
+
auto?: boolean;
|
|
21
|
+
/** Minimum number of §9 lessons before consolidation is useful (default: 5) */
|
|
22
|
+
minLessons?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ConsolidateResult {
|
|
26
|
+
/** Whether the file was actually written */
|
|
27
|
+
written: boolean;
|
|
28
|
+
/** Stats before consolidation */
|
|
29
|
+
before: ConstitutionStats;
|
|
30
|
+
/** Stats after consolidation */
|
|
31
|
+
after: ConstitutionStats;
|
|
32
|
+
/** Path to the backup file */
|
|
33
|
+
backupPath: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Threshold Warning ────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if §9 has grown past the warning threshold.
|
|
40
|
+
* Call this after appending lessons to warn the user when consolidation is needed.
|
|
41
|
+
*/
|
|
42
|
+
export function checkConsolidationNeeded(
|
|
43
|
+
projectRoot: string,
|
|
44
|
+
lessonCount: number,
|
|
45
|
+
warnAt = 8
|
|
46
|
+
): void {
|
|
47
|
+
if (lessonCount >= warnAt) {
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.yellow(
|
|
50
|
+
`\n ⚠ §9 has ${lessonCount} accumulated lessons — consider running \`ai-spec init --consolidate\` to prune.`
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Consolidator ─────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export class ConstitutionConsolidator {
|
|
59
|
+
constructor(private provider: AIProvider) {}
|
|
60
|
+
|
|
61
|
+
async consolidate(
|
|
62
|
+
projectRoot: string,
|
|
63
|
+
opts: ConsolidateOptions = {}
|
|
64
|
+
): Promise<ConsolidateResult> {
|
|
65
|
+
const minLessons = opts.minLessons ?? 5;
|
|
66
|
+
const constitutionPath = path.join(projectRoot, CONSTITUTION_FILE);
|
|
67
|
+
|
|
68
|
+
// ── Load constitution ───────────────────────────────────────────────────
|
|
69
|
+
if (!(await fs.pathExists(constitutionPath))) {
|
|
70
|
+
throw new Error(`No constitution file found at ${constitutionPath}. Run \`ai-spec init\` first.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const original = await fs.readFile(constitutionPath, "utf-8");
|
|
74
|
+
const before = parseConstitutionStats(original);
|
|
75
|
+
|
|
76
|
+
console.log(chalk.blue("\n─── Constitution Consolidation ──────────────────"));
|
|
77
|
+
console.log(chalk.gray(` File : ${CONSTITUTION_FILE}`));
|
|
78
|
+
console.log(chalk.gray(` Size : ${before.totalLines} lines`));
|
|
79
|
+
console.log(chalk.gray(` §9 items: ${before.lessonCount} accumulated lessons`));
|
|
80
|
+
|
|
81
|
+
if (before.lessonCount < minLessons) {
|
|
82
|
+
console.log(
|
|
83
|
+
chalk.green(
|
|
84
|
+
`\n ✔ §9 has only ${before.lessonCount} lesson(s) — no consolidation needed yet (threshold: ${minLessons}).`
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
return { written: false, before, after: before, backupPath: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Generate consolidated version ───────────────────────────────────────
|
|
91
|
+
console.log(chalk.cyan(`\n Consolidating ${before.lessonCount} lesson(s) with AI...`));
|
|
92
|
+
|
|
93
|
+
const prompt = buildConsolidatePrompt(original, before.lessonCount);
|
|
94
|
+
let consolidated: string;
|
|
95
|
+
try {
|
|
96
|
+
const raw = await this.provider.generate(prompt, consolidateSystemPrompt);
|
|
97
|
+
// Strip markdown fences if present
|
|
98
|
+
consolidated = raw
|
|
99
|
+
.replace(/^```(?:markdown|md)?\n?/im, "")
|
|
100
|
+
.replace(/\n?```\s*$/im, "")
|
|
101
|
+
.trim();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new Error(`AI consolidation failed: ${(err as Error).message}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const after = parseConstitutionStats(consolidated);
|
|
107
|
+
|
|
108
|
+
// ── Show diff ───────────────────────────────────────────────────────────
|
|
109
|
+
const diff = computeDiff(original, consolidated);
|
|
110
|
+
console.log(chalk.blue("\n Changes preview:"));
|
|
111
|
+
printDiff(diff, 4);
|
|
112
|
+
printDiffSummary(diff);
|
|
113
|
+
|
|
114
|
+
console.log(chalk.cyan("\n After consolidation:"));
|
|
115
|
+
console.log(chalk.gray(` Size : ${after.totalLines} lines (was ${before.totalLines})`));
|
|
116
|
+
console.log(chalk.gray(` §9 items: ${after.lessonCount} remaining (was ${before.lessonCount})`));
|
|
117
|
+
|
|
118
|
+
const liftedCount = Math.max(0, before.lessonCount - after.lessonCount);
|
|
119
|
+
if (liftedCount > 0) {
|
|
120
|
+
console.log(chalk.green(` ✔ ~${liftedCount} lesson(s) lifted into §1–§8 or removed`));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (opts.dryRun) {
|
|
124
|
+
console.log(chalk.yellow("\n [dry-run] No changes written."));
|
|
125
|
+
return { written: false, before, after, backupPath: null };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Backup ──────────────────────────────────────────────────────────────
|
|
129
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-");
|
|
130
|
+
const backupName = `.ai-spec-constitution.backup-${timestamp}.md`;
|
|
131
|
+
const backupPath = path.join(projectRoot, backupName);
|
|
132
|
+
await fs.writeFile(backupPath, original, "utf-8");
|
|
133
|
+
console.log(chalk.gray(`\n Backup : ${backupName}`));
|
|
134
|
+
|
|
135
|
+
// ── Write ───────────────────────────────────────────────────────────────
|
|
136
|
+
await fs.writeFile(constitutionPath, consolidated, "utf-8");
|
|
137
|
+
console.log(chalk.green(` ✔ Constitution updated: ${CONSTITUTION_FILE}`));
|
|
138
|
+
|
|
139
|
+
return { written: true, before, after, backupPath };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as fs from "fs-extra";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { AIProvider } from "./spec-generator";
|
|
5
|
+
import { ContextLoader, ProjectContext } from "./context-loader";
|
|
6
|
+
import { constitutionSystemPrompt } from "../prompts/constitution.prompt";
|
|
7
|
+
|
|
8
|
+
export const CONSTITUTION_FILE = ".ai-spec-constitution.md";
|
|
9
|
+
|
|
10
|
+
export class ConstitutionGenerator {
|
|
11
|
+
constructor(private provider: AIProvider) {}
|
|
12
|
+
|
|
13
|
+
async generate(projectRoot: string): Promise<string> {
|
|
14
|
+
const loader = new ContextLoader(projectRoot);
|
|
15
|
+
const context = await loader.loadProjectContext();
|
|
16
|
+
|
|
17
|
+
const prompt = buildConstitutionPrompt(context, projectRoot);
|
|
18
|
+
return this.provider.generate(prompt, constitutionSystemPrompt);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async saveConstitution(projectRoot: string, content: string): Promise<string> {
|
|
22
|
+
const filePath = path.join(projectRoot, CONSTITUTION_FILE);
|
|
23
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildConstitutionPrompt(context: ProjectContext, projectRoot: string): string {
|
|
29
|
+
const parts: string[] = [
|
|
30
|
+
"Analyze this project and generate its Project Constitution.\n",
|
|
31
|
+
`=== Tech Stack ===\n${context.techStack.join(", ") || "unknown"}\n`,
|
|
32
|
+
`=== Dependencies (top 30) ===\n${context.dependencies.slice(0, 30).join(", ")}\n`,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
if (context.apiStructure.length > 0) {
|
|
36
|
+
parts.push(`=== API/Route Files ===\n${context.apiStructure.join("\n")}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (context.routeSummary) {
|
|
40
|
+
parts.push(`=== Route Code Samples ===\n${context.routeSummary}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (context.schema) {
|
|
44
|
+
parts.push(`=== Prisma Schema ===\n${context.schema.slice(0, 4000)}\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (context.errorPatterns) {
|
|
48
|
+
parts.push(`=== Error Handling Patterns ===\n${context.errorPatterns}\n`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
|
|
52
|
+
const grouped = context.sharedConfigFiles.reduce(
|
|
53
|
+
(acc, f) => {
|
|
54
|
+
(acc[f.category] ??= []).push(f);
|
|
55
|
+
return acc;
|
|
56
|
+
},
|
|
57
|
+
{} as Record<string, typeof context.sharedConfigFiles>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const sections: string[] = [];
|
|
61
|
+
for (const [category, files] of Object.entries(grouped)) {
|
|
62
|
+
sections.push(`--- ${category} ---`);
|
|
63
|
+
for (const f of files!) {
|
|
64
|
+
sections.push(`File: ${f.path}\n${f.preview.slice(0, 600)}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
parts.push(`=== Existing Shared Config Files (Append-Only — NEVER Recreate) ===\n${sections.join("\n")}\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return parts.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function loadConstitution(projectRoot: string): Promise<string | undefined> {
|
|
74
|
+
const filePath = path.join(projectRoot, CONSTITUTION_FILE);
|
|
75
|
+
if (await fs.pathExists(filePath)) {
|
|
76
|
+
return fs.readFile(filePath, "utf-8");
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function printConstitutionHint(exists: boolean): void {
|
|
82
|
+
if (!exists) {
|
|
83
|
+
console.log(
|
|
84
|
+
chalk.yellow(
|
|
85
|
+
" ⚡ Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|