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,49 @@
|
|
|
1
|
+
import * as fs from "fs-extra";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
|
|
5
|
+
const KEY_STORE_FILE = path.join(os.homedir(), ".ai-spec-keys.json");
|
|
6
|
+
|
|
7
|
+
type KeyStore = Record<string, string>;
|
|
8
|
+
|
|
9
|
+
async function readStore(): Promise<KeyStore> {
|
|
10
|
+
try {
|
|
11
|
+
if (await fs.pathExists(KEY_STORE_FILE)) {
|
|
12
|
+
return await fs.readJson(KEY_STORE_FILE);
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
// ignore corrupt file
|
|
16
|
+
}
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function writeStore(store: KeyStore): Promise<void> {
|
|
21
|
+
await fs.writeJson(KEY_STORE_FILE, store, { spaces: 2 });
|
|
22
|
+
// Restrict permissions to owner only (600)
|
|
23
|
+
await fs.chmod(KEY_STORE_FILE, 0o600);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function getSavedKey(provider: string): Promise<string | undefined> {
|
|
27
|
+
const store = await readStore();
|
|
28
|
+
return store[provider] || undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function saveKey(provider: string, key: string): Promise<void> {
|
|
32
|
+
const store = await readStore();
|
|
33
|
+
store[provider] = key;
|
|
34
|
+
await writeStore(store);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function clearAllKeys(): Promise<void> {
|
|
38
|
+
if (await fs.pathExists(KEY_STORE_FILE)) {
|
|
39
|
+
await fs.remove(KEY_STORE_FILE);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function clearKey(provider: string): Promise<void> {
|
|
44
|
+
const store = await readStore();
|
|
45
|
+
delete store[provider];
|
|
46
|
+
await writeStore(store);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { KEY_STORE_FILE };
|
|
@@ -0,0 +1,171 @@
|
|
|
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 { parseConstitutionStats } from "../prompts/consolidate.prompt";
|
|
7
|
+
|
|
8
|
+
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface ReviewIssue {
|
|
11
|
+
/** Short description of the issue */
|
|
12
|
+
description: string;
|
|
13
|
+
/** Which file or area */
|
|
14
|
+
location?: string;
|
|
15
|
+
/** Category: bug / pattern / style / security / performance */
|
|
16
|
+
category: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Extract Issues from Review ─────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse review text to extract issues from the "⚠️ 问题" section.
|
|
23
|
+
* Returns up to 10 issues (to keep constitution append manageable).
|
|
24
|
+
*/
|
|
25
|
+
export function extractIssuesFromReview(reviewText: string): ReviewIssue[] {
|
|
26
|
+
const issues: ReviewIssue[] = [];
|
|
27
|
+
|
|
28
|
+
// Find the issues section (between ⚠️ and 💡 or 📊)
|
|
29
|
+
const issuesMatch = reviewText.match(
|
|
30
|
+
/## ⚠[^\n]*\n([\s\S]*?)(?=## [💡📊]|\n*$)/i
|
|
31
|
+
);
|
|
32
|
+
if (!issuesMatch) return issues;
|
|
33
|
+
|
|
34
|
+
const section = issuesMatch[1];
|
|
35
|
+
const lines = section.split("\n");
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
40
|
+
|
|
41
|
+
// Match list items: "- issue text" or "1. issue text" or "**Category**: text"
|
|
42
|
+
const itemMatch = trimmed.match(/^[-*\d]+[.)]?\s*(.+)/);
|
|
43
|
+
if (itemMatch) {
|
|
44
|
+
const desc = itemMatch[1].replace(/\*\*/g, "").trim();
|
|
45
|
+
if (desc.length > 10) {
|
|
46
|
+
issues.push({
|
|
47
|
+
description: desc.slice(0, 200),
|
|
48
|
+
category: categorizeIssue(desc),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return issues.slice(0, 10);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function categorizeIssue(text: string): string {
|
|
58
|
+
const lower = text.toLowerCase();
|
|
59
|
+
if (lower.includes("security") || lower.includes("auth") || lower.includes("注入") || lower.includes("xss") || lower.includes("sql")) return "security";
|
|
60
|
+
if (lower.includes("performance") || lower.includes("慢") || lower.includes("性能") || lower.includes("n+1")) return "performance";
|
|
61
|
+
if (lower.includes("error") || lower.includes("异常") || lower.includes("crash") || lower.includes("bug")) return "bug";
|
|
62
|
+
if (lower.includes("pattern") || lower.includes("模式") || lower.includes("convention") || lower.includes("命名")) return "pattern";
|
|
63
|
+
return "general";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Append to Constitution ────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const MEMORY_SECTION_HEADER = "\n\n## 9. 积累教训 (Accumulated Lessons)\n";
|
|
69
|
+
const MEMORY_SECTION_MARKER = "## 9. 积累教训";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Append review issues to the project constitution as accumulated lessons.
|
|
73
|
+
* Creates the section if it doesn't exist; appends if it does.
|
|
74
|
+
* Deduplicates by checking if a similar lesson already exists.
|
|
75
|
+
*/
|
|
76
|
+
export async function appendLessonsToConstitution(
|
|
77
|
+
projectRoot: string,
|
|
78
|
+
issues: ReviewIssue[]
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
if (issues.length === 0) return;
|
|
81
|
+
|
|
82
|
+
const constitutionPath = path.join(projectRoot, CONSTITUTION_FILE);
|
|
83
|
+
let content = "";
|
|
84
|
+
try {
|
|
85
|
+
content = await fs.readFile(constitutionPath, "utf-8");
|
|
86
|
+
} catch {
|
|
87
|
+
console.log(chalk.gray(" No constitution file — skipping knowledge memory."));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if section 9 already exists
|
|
92
|
+
const hasMemorySection = content.includes(MEMORY_SECTION_MARKER);
|
|
93
|
+
|
|
94
|
+
// Build new lesson entries with date stamp
|
|
95
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
96
|
+
const newEntries: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (const issue of issues) {
|
|
99
|
+
// Simple dedup: check if a similar line already exists (case-insensitive substring)
|
|
100
|
+
const normalized = issue.description.toLowerCase().slice(0, 50);
|
|
101
|
+
if (content.toLowerCase().includes(normalized)) continue;
|
|
102
|
+
|
|
103
|
+
const badge = issue.category === "security" ? "🔒" :
|
|
104
|
+
issue.category === "performance" ? "⚡" :
|
|
105
|
+
issue.category === "bug" ? "🐛" :
|
|
106
|
+
issue.category === "pattern" ? "📐" : "📝";
|
|
107
|
+
newEntries.push(`- ${badge} **[${date}]** ${issue.description}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (newEntries.length === 0) {
|
|
111
|
+
console.log(chalk.gray(" No new lessons to add (all deduplicated)."));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let updatedContent: string;
|
|
116
|
+
if (hasMemorySection) {
|
|
117
|
+
// Append to existing section — find the end of section 9
|
|
118
|
+
// Strategy: find "## 9." then insert before the next "## " or EOF
|
|
119
|
+
const sectionStart = content.indexOf(MEMORY_SECTION_MARKER);
|
|
120
|
+
const afterHeader = sectionStart + MEMORY_SECTION_HEADER.length;
|
|
121
|
+
// Find next section header after section 9
|
|
122
|
+
const nextSectionMatch = content.slice(afterHeader).match(/\n## \d/);
|
|
123
|
+
const insertPos = nextSectionMatch
|
|
124
|
+
? afterHeader + nextSectionMatch.index!
|
|
125
|
+
: content.length;
|
|
126
|
+
updatedContent =
|
|
127
|
+
content.slice(0, insertPos) +
|
|
128
|
+
newEntries.join("\n") + "\n" +
|
|
129
|
+
content.slice(insertPos);
|
|
130
|
+
} else {
|
|
131
|
+
// Append new section at the end
|
|
132
|
+
updatedContent = content + MEMORY_SECTION_HEADER + newEntries.join("\n") + "\n";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await fs.writeFile(constitutionPath, updatedContent, "utf-8");
|
|
136
|
+
console.log(chalk.green(` ✔ ${newEntries.length} lesson(s) appended to constitution (§9).`));
|
|
137
|
+
|
|
138
|
+
// Warn when §9 is getting long
|
|
139
|
+
const stats = parseConstitutionStats(updatedContent);
|
|
140
|
+
if (stats.lessonCount >= 8) {
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.yellow(
|
|
143
|
+
` ⚠ §9 now has ${stats.lessonCount} accumulated lessons. Run \`ai-spec init --consolidate\` to prune and rebase.`
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Full knowledge memory flow: extract issues from review → append to constitution.
|
|
151
|
+
*/
|
|
152
|
+
export async function accumulateReviewKnowledge(
|
|
153
|
+
provider: AIProvider,
|
|
154
|
+
projectRoot: string,
|
|
155
|
+
reviewText: string
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
console.log(chalk.blue("\n─── Knowledge Memory ────────────────────────────"));
|
|
158
|
+
|
|
159
|
+
const issues = extractIssuesFromReview(reviewText);
|
|
160
|
+
if (issues.length === 0) {
|
|
161
|
+
console.log(chalk.gray(" No actionable issues found in review. Skipping."));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(chalk.gray(` Extracted ${issues.length} issue(s) from review:`));
|
|
166
|
+
for (const issue of issues) {
|
|
167
|
+
console.log(chalk.gray(` - [${issue.category}] ${issue.description.slice(0, 80)}`));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await appendLessonsToConstitution(projectRoot, issues);
|
|
171
|
+
}
|