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.
Files changed (60) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/README.md +1215 -146
  3. package/RELEASE_LOG.md +1489 -0
  4. package/cli/index.ts +1981 -0
  5. package/cli/welcome.ts +151 -0
  6. package/core/code-generator.ts +757 -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,259 @@
1
+ import chalk from "chalk";
2
+ import { execSync } from "child_process";
3
+ import * as path from "path";
4
+ import * as fs from "fs-extra";
5
+ import { AIProvider } from "./spec-generator";
6
+ import {
7
+ reviewSystemPrompt,
8
+ reviewArchitectureSystemPrompt,
9
+ reviewImplementationSystemPrompt,
10
+ } from "../prompts/codegen.prompt";
11
+
12
+ // ─── Review History ────────────────────────────────────────────────────────────
13
+
14
+ interface ReviewHistoryEntry {
15
+ date: string;
16
+ specFile: string;
17
+ score: number;
18
+ topIssues: string[];
19
+ }
20
+
21
+ const REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
22
+
23
+ async function loadReviewHistory(projectRoot: string): Promise<ReviewHistoryEntry[]> {
24
+ const historyPath = path.join(projectRoot, REVIEW_HISTORY_FILE);
25
+ try {
26
+ if (await fs.pathExists(historyPath)) {
27
+ return await fs.readJson(historyPath);
28
+ }
29
+ } catch {
30
+ // ignore
31
+ }
32
+ return [];
33
+ }
34
+
35
+ async function appendReviewHistory(
36
+ projectRoot: string,
37
+ entry: ReviewHistoryEntry
38
+ ): Promise<void> {
39
+ const historyPath = path.join(projectRoot, REVIEW_HISTORY_FILE);
40
+ const existing = await loadReviewHistory(projectRoot);
41
+ // Keep the last 20 entries
42
+ const updated = [...existing, entry].slice(-20);
43
+ try {
44
+ await fs.writeJson(historyPath, updated, { spaces: 2 });
45
+ } catch {
46
+ // ignore — history is non-critical
47
+ }
48
+ }
49
+
50
+ /** Extract numeric score from a review result string (looks for "Score: X/10") */
51
+ function extractScore(reviewText: string): number {
52
+ const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
53
+ return match ? parseFloat(match[1]) : 0;
54
+ }
55
+
56
+ /** Extract top issue lines from a review result (lines starting with - or · under ⚠️ section) */
57
+ function extractTopIssues(reviewText: string): string[] {
58
+ const issuesSection = reviewText.match(/##.*?问题.*?\n([\s\S]*?)(?=##|$)/i)?.[1] ?? "";
59
+ return issuesSection
60
+ .split("\n")
61
+ .filter((l) => /^[-·•*]/.test(l.trim()))
62
+ .map((l) => l.replace(/^[-·•*]\s*/, "").trim())
63
+ .filter(Boolean)
64
+ .slice(0, 3);
65
+ }
66
+
67
+ /** Format recent review history as a context string for Pass 2 */
68
+ function buildHistoryContext(history: ReviewHistoryEntry[]): string {
69
+ if (history.length === 0) return "";
70
+ const recent = history.slice(-5);
71
+ const lines = ["\n=== 历史审查问题 (Past Review Issues — check if any recur) ==="];
72
+ for (const entry of recent) {
73
+ lines.push(`\n[${entry.date}] ${path.basename(entry.specFile)} — Score: ${entry.score}/10`);
74
+ entry.topIssues.forEach((issue) => lines.push(` · ${issue}`));
75
+ }
76
+ return lines.join("\n") + "\n";
77
+ }
78
+
79
+ // ─── CodeReviewer ─────────────────────────────────────────────────────────────
80
+
81
+ export class CodeReviewer {
82
+ constructor(
83
+ private provider: AIProvider,
84
+ private projectRoot: string = process.cwd()
85
+ ) {}
86
+
87
+ private getGitDiff(): string {
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ const silent: any = { encoding: "utf-8", stdio: "pipe" };
90
+ try {
91
+ execSync("git rev-parse --is-inside-work-tree", silent);
92
+ } catch {
93
+ return "";
94
+ }
95
+ try {
96
+ let diff: string = execSync("git diff --cached", silent) as string;
97
+ if (!diff.trim()) diff = execSync("git diff HEAD", silent) as string;
98
+ if (!diff.trim()) diff = execSync("git diff", silent) as string;
99
+ return diff;
100
+ } catch {
101
+ return "";
102
+ }
103
+ }
104
+
105
+ private getDiffStats(diff: string): { files: number; added: number; removed: number } {
106
+ const lines = diff.split("\n");
107
+ return {
108
+ files: lines.filter((l) => l.startsWith("diff --git")).length,
109
+ added: lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length,
110
+ removed: lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Two-pass review:
116
+ * Pass 1 — architecture (spec compliance, layer separation, auth)
117
+ * Pass 2 — implementation details (validation, error handling, edge cases)
118
+ * + historical issue recurrence check
119
+ *
120
+ * Falls back to single-pass if the two-pass flag is not set.
121
+ */
122
+ private async runTwoPassReview(
123
+ specContent: string,
124
+ codeContext: string,
125
+ specFile?: string
126
+ ): Promise<string> {
127
+ console.log(chalk.gray(" Pass 1/2: Architecture review..."));
128
+
129
+ // ── Pass 1: Architecture ──────────────────────────────────────────────────
130
+ const archPrompt = `Review the architecture of this change.
131
+
132
+ === Feature Spec ===
133
+ ${specContent || "(No spec — review for general code quality)"}
134
+
135
+ === Code ===
136
+ ${codeContext}`;
137
+
138
+ const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
139
+ console.log(chalk.gray(" Pass 2/2: Implementation review..."));
140
+
141
+ // ── Pass 2: Implementation + History ─────────────────────────────────────
142
+ const history = await loadReviewHistory(this.projectRoot);
143
+ const historyContext = buildHistoryContext(history);
144
+
145
+ const implPrompt = `Review the implementation details of this change.
146
+
147
+ === Feature Spec ===
148
+ ${specContent || "(No spec — review for general code quality)"}
149
+
150
+ === Code ===
151
+ ${codeContext}
152
+
153
+ === Architecture Review (Pass 1 — do NOT repeat these findings) ===
154
+ ${archReview}
155
+ ${historyContext}`;
156
+
157
+ const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
158
+
159
+ // ── Combine ───────────────────────────────────────────────────────────────
160
+ const combined = `${archReview}\n\n${"─".repeat(52)}\n\n${implReview}`;
161
+
162
+ // ── Persist history ───────────────────────────────────────────────────────
163
+ const score = extractScore(implReview) || extractScore(archReview);
164
+ const topIssues = extractTopIssues(implReview);
165
+ if (score > 0 && specFile) {
166
+ await appendReviewHistory(this.projectRoot, {
167
+ date: new Date().toISOString().slice(0, 10),
168
+ specFile: path.relative(this.projectRoot, specFile),
169
+ score,
170
+ topIssues,
171
+ });
172
+ }
173
+
174
+ return combined;
175
+ }
176
+
177
+ async reviewCode(specContent: string, specFile?: string): Promise<string> {
178
+ console.log(chalk.cyan("\n─── Automated Code Review ───────────────────────"));
179
+
180
+ const diff = this.getGitDiff();
181
+ if (!diff.trim()) {
182
+ console.log(
183
+ chalk.yellow(" No git diff found. Stage or commit changes first, then run review.")
184
+ );
185
+ console.log(chalk.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
186
+ return "No changes";
187
+ }
188
+
189
+ const { files, added, removed } = this.getDiffStats(diff);
190
+ console.log(
191
+ chalk.gray(` Diff: ${files} file(s), ${chalk.green("+" + added)} ${chalk.red("-" + removed)}`)
192
+ );
193
+ console.log(
194
+ chalk.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
195
+ );
196
+
197
+ const codeContext = diff.slice(0, 10000);
198
+ const reviewResult = await this.runTwoPassReview(specContent, codeContext, specFile);
199
+
200
+ console.log(chalk.cyan("\n─── Review Result ───────────────────────────────"));
201
+ console.log(reviewResult);
202
+ console.log(chalk.cyan("─────────────────────────────────────────────────\n"));
203
+
204
+ return reviewResult;
205
+ }
206
+
207
+ /**
208
+ * Review directly from generated file contents (for api mode where git diff is empty).
209
+ */
210
+ async reviewFiles(
211
+ specContent: string,
212
+ filePaths: string[],
213
+ workingDir: string,
214
+ specFile?: string
215
+ ): Promise<string> {
216
+ console.log(chalk.cyan("\n─── Automated Code Review (file-based) ─────────"));
217
+ console.log(chalk.gray(` Reviewing ${filePaths.length} generated file(s)...`));
218
+ console.log(
219
+ chalk.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
220
+ );
221
+
222
+ let filesSection = "";
223
+ for (const filePath of filePaths) {
224
+ const fullPath = path.join(workingDir, filePath);
225
+ try {
226
+ const content = await fs.readFile(fullPath, "utf-8");
227
+ filesSection += `\n\n=== ${filePath} ===\n${content.slice(0, 3000)}`;
228
+ if (content.length > 3000) filesSection += `\n... (truncated, ${content.length} chars total)`;
229
+ } catch {
230
+ filesSection += `\n\n=== ${filePath} ===\n(file not found)`;
231
+ }
232
+ }
233
+
234
+ const reviewResult = await this.runTwoPassReview(specContent, filesSection, specFile);
235
+
236
+ console.log(chalk.cyan("\n─── Review Result ───────────────────────────────"));
237
+ console.log(reviewResult);
238
+ console.log(chalk.cyan("─────────────────────────────────────────────────\n"));
239
+
240
+ return reviewResult;
241
+ }
242
+
243
+ /** Print score trend from history (last N reviews) */
244
+ async printScoreTrend(limit = 5): Promise<void> {
245
+ const history = await loadReviewHistory(this.projectRoot);
246
+ if (history.length === 0) {
247
+ console.log(chalk.gray(" No review history yet."));
248
+ return;
249
+ }
250
+ const recent = history.slice(-limit);
251
+ console.log(chalk.cyan("\n─── Review Score Trend ──────────────────────────"));
252
+ for (const entry of recent) {
253
+ const bar = "█".repeat(entry.score) + "░".repeat(10 - entry.score);
254
+ const color = entry.score >= 8 ? chalk.green : entry.score >= 6 ? chalk.yellow : chalk.red;
255
+ console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")} ${path.basename(entry.specFile)}`);
256
+ }
257
+ console.log(chalk.cyan("─────────────────────────────────────────────────"));
258
+ }
259
+ }
@@ -0,0 +1,99 @@
1
+ import chalk from "chalk";
2
+ import { AIProvider } from "./spec-generator";
3
+ import { specAssessSystemPrompt } from "../prompts/spec-assess.prompt";
4
+
5
+ // ─── Types ─────────────────────────────────────────────────────────────────────
6
+
7
+ export interface SpecAssessment {
8
+ coverageScore: number;
9
+ clarityScore: number;
10
+ constitutionScore: number;
11
+ overallScore: number;
12
+ issues: string[];
13
+ suggestions: string[];
14
+ dslExtractable: boolean;
15
+ }
16
+
17
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
18
+
19
+ function scoreBar(score: number): string {
20
+ const filled = Math.round(score);
21
+ const bar = chalk.green("█".repeat(filled)) + chalk.gray("░".repeat(10 - filled));
22
+ const color = score >= 8 ? chalk.green : score >= 6 ? chalk.yellow : chalk.red;
23
+ return `[${bar}] ${color(score + "/10")}`;
24
+ }
25
+
26
+ function parseAssessment(raw: string): SpecAssessment | null {
27
+ // Strip markdown fences if present
28
+ const stripped = raw.replace(/```(?:json)?\n?/g, "").replace(/```\s*$/g, "").trim();
29
+ try {
30
+ const parsed = JSON.parse(stripped);
31
+ if (
32
+ typeof parsed.coverageScore === "number" &&
33
+ typeof parsed.clarityScore === "number" &&
34
+ typeof parsed.overallScore === "number"
35
+ ) {
36
+ return parsed as SpecAssessment;
37
+ }
38
+ } catch {
39
+ // fall through
40
+ }
41
+ return null;
42
+ }
43
+
44
+ // ─── Main export ───────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Run a pre-Approval-Gate quality check on the spec.
48
+ * Advisory only — does not block the flow regardless of scores.
49
+ * Returns null if the AI call fails (graceful degradation).
50
+ */
51
+ export async function assessSpec(
52
+ provider: AIProvider,
53
+ spec: string,
54
+ constitution?: string
55
+ ): Promise<SpecAssessment | null> {
56
+ const prompt = `Assess the following feature specification.
57
+ ${constitution ? `\n=== Project Constitution (check consistency against this) ===\n${constitution}\n` : ""}
58
+ === Feature Spec ===
59
+ ${spec}`;
60
+
61
+ try {
62
+ const raw = await provider.generate(prompt, specAssessSystemPrompt);
63
+ return parseAssessment(raw);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Print the spec assessment panel to stdout.
71
+ */
72
+ export function printSpecAssessment(assessment: SpecAssessment): void {
73
+ console.log(chalk.blue("\n─── Spec Quality Assessment ─────────────────────"));
74
+ console.log(` Coverage ${scoreBar(assessment.coverageScore)} error handling, edge cases, auth`);
75
+ console.log(` Clarity ${scoreBar(assessment.clarityScore)} API contracts, response shapes`);
76
+ console.log(` Constitution${scoreBar(assessment.constitutionScore)} naming, error codes, conventions`);
77
+ console.log(chalk.bold(` Overall ${scoreBar(assessment.overallScore)}`));
78
+
79
+ if (!assessment.dslExtractable) {
80
+ console.log(
81
+ chalk.yellow(
82
+ "\n ⚠ DSL extraction may be unreliable — clarityScore < 6 or no structured API section."
83
+ )
84
+ );
85
+ console.log(chalk.gray(" Consider adding explicit request/response shapes before proceeding."));
86
+ }
87
+
88
+ if (assessment.issues.length > 0) {
89
+ console.log(chalk.yellow(`\n Issues found (${assessment.issues.length}):`));
90
+ assessment.issues.forEach((issue) => console.log(chalk.yellow(` · ${issue}`)));
91
+ }
92
+
93
+ if (assessment.suggestions.length > 0) {
94
+ console.log(chalk.cyan("\n Suggestions:"));
95
+ assessment.suggestions.forEach((s) => console.log(chalk.cyan(` 💡 ${s}`)));
96
+ }
97
+
98
+ console.log(chalk.blue("─────────────────────────────────────────────────"));
99
+ }