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,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
+ }