ai-spec-dev 0.46.0 → 0.56.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/README.md +60 -30
- package/cli/commands/config.ts +129 -1
- package/cli/commands/create.ts +14 -0
- package/cli/commands/fix-history.ts +176 -0
- package/cli/commands/init.ts +36 -1
- package/cli/index.ts +2 -6
- package/cli/pipeline/helpers.ts +6 -0
- package/cli/pipeline/multi-repo.ts +300 -26
- package/cli/pipeline/single-repo.ts +103 -2
- package/cli/utils.ts +23 -0
- package/core/code-generator.ts +63 -14
- package/core/cross-stack-verifier.ts +482 -0
- package/core/fix-history.ts +333 -0
- package/core/import-fixer.ts +827 -0
- package/core/import-verifier.ts +569 -0
- package/core/knowledge-memory.ts +55 -6
- package/core/self-evaluator.ts +44 -7
- package/core/spec-generator.ts +3 -3
- package/core/types-generator.ts +2 -2
- package/dist/cli/index.js +3968 -2353
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3810 -2195
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +249 -128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +249 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/cross-stack-verifier.test.ts +402 -0
- package/tests/fix-history.test.ts +335 -0
- package/tests/import-fixer.test.ts +944 -0
- package/tests/import-verifier.test.ts +420 -0
- package/tests/knowledge-memory.test.ts +40 -0
- package/tests/self-evaluator.test.ts +97 -0
- package/.ai-spec-workspace.json +0 -17
- package/.ai-spec.json +0 -7
- package/cli/commands/model.ts +0 -152
- package/cli/commands/scan.ts +0 -99
- package/cli/commands/workspace.ts +0 -219
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<a href="https://github.com/hzhongzhong/ai-spec"><img src="https://img.shields.io/badge/GitHub-ai--spec-181717?logo=github" alt="GitHub" /></a>
|
|
14
14
|
<a href="https://www.npmjs.com/package/ai-spec-dev"><img src="https://img.shields.io/npm/v/ai-spec-dev?color=cb3837&logo=npm" alt="npm" /></a>
|
|
15
|
-
<img src="https://img.shields.io/badge/version-0.
|
|
16
|
-
<img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<img src="https://img.shields.io/badge/version-0.55.0-blue" alt="version" />
|
|
16
|
+
<img src="https://img.shields.io/badge/tests-913%20passed-brightgreen" alt="tests" />
|
|
17
17
|
<img src="https://img.shields.io/badge/providers-9-orange" alt="providers" />
|
|
18
18
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
|
|
19
19
|
</p>
|
|
@@ -122,20 +122,35 @@ ai-spec create "Add login functionality to user module"
|
|
|
122
122
|
### Commands
|
|
123
123
|
|
|
124
124
|
```
|
|
125
|
-
|
|
126
|
-
ai-spec init
|
|
127
|
-
ai-spec
|
|
128
|
-
ai-spec
|
|
129
|
-
ai-spec
|
|
130
|
-
ai-spec
|
|
131
|
-
ai-spec
|
|
132
|
-
ai-spec
|
|
133
|
-
ai-spec
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
ai-spec
|
|
137
|
-
|
|
138
|
-
|
|
125
|
+
# Core workflow
|
|
126
|
+
ai-spec init Register repos, generate constitutions (auto-scans projects)
|
|
127
|
+
ai-spec init --add-repo Quick-add a single repo
|
|
128
|
+
ai-spec init --status Show registered repos and constitution health
|
|
129
|
+
ai-spec create [idea] Full pipeline: spec → DSL → codegen → review
|
|
130
|
+
ai-spec create [idea] --openapi Also auto-generate OpenAPI 3.1.0 YAML after DSL
|
|
131
|
+
ai-spec create [idea] --types Also auto-generate TypeScript types after DSL
|
|
132
|
+
ai-spec update [change] Incremental: modify spec → re-extract DSL → regen affected files
|
|
133
|
+
ai-spec review [file] 3-pass AI code review (architecture + implementation + impact)
|
|
134
|
+
|
|
135
|
+
# Knowledge & constitution
|
|
136
|
+
ai-spec learn [lesson] Inject a lesson directly into constitution §9
|
|
137
|
+
|
|
138
|
+
# DSL-derived artifacts
|
|
139
|
+
ai-spec export DSL → OpenAPI 3.1.0 YAML/JSON
|
|
140
|
+
ai-spec types DSL → TypeScript types (models + endpoint types + API_ENDPOINTS)
|
|
141
|
+
ai-spec mock DSL → Mock server + MSW handlers + proxy config
|
|
142
|
+
|
|
143
|
+
# Configuration (run without flags for interactive model/provider picker)
|
|
144
|
+
ai-spec config Interactive provider/model setup (merged from `model`)
|
|
145
|
+
ai-spec config --show Print current configuration
|
|
146
|
+
ai-spec config --list List all available providers and models
|
|
147
|
+
|
|
148
|
+
# Observability
|
|
149
|
+
ai-spec logs [runId] View run history or stage breakdown
|
|
150
|
+
ai-spec trend Harness score trend across runs
|
|
151
|
+
ai-spec dashboard Generate static HTML harness dashboard
|
|
152
|
+
ai-spec restore <runId> Rollback all files modified by a specific run
|
|
153
|
+
ai-spec vcr list List VCR recordings
|
|
139
154
|
```
|
|
140
155
|
|
|
141
156
|
### Architecture
|
|
@@ -318,20 +333,35 @@ ai-spec create "给用户模块增加登录功能"
|
|
|
318
333
|
### 命令总览
|
|
319
334
|
|
|
320
335
|
```
|
|
321
|
-
|
|
322
|
-
ai-spec init
|
|
323
|
-
ai-spec
|
|
324
|
-
ai-spec
|
|
325
|
-
ai-spec
|
|
326
|
-
ai-spec
|
|
327
|
-
ai-spec
|
|
328
|
-
ai-spec
|
|
329
|
-
ai-spec
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
ai-spec
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
# 核心开发流程
|
|
337
|
+
ai-spec init 注册仓库,生成项目宪法 + 全局宪法(自动扫描项目)
|
|
338
|
+
ai-spec init --add-repo 快速添加单个仓库
|
|
339
|
+
ai-spec init --status 查看已注册仓库和宪法状态
|
|
340
|
+
ai-spec create [idea] 完整流水线:spec → DSL → 代码生成 → 审查
|
|
341
|
+
ai-spec create [idea] --openapi DSL 提取后自动生成 OpenAPI 3.1.0 YAML
|
|
342
|
+
ai-spec create [idea] --types DSL 提取后自动生成 TypeScript 类型文件
|
|
343
|
+
ai-spec update [change] 增量更新:修改 Spec → 重提取 DSL → 重新生成受影响文件
|
|
344
|
+
ai-spec review [file] 3-pass AI 代码审查(架构 + 实现 + 影响面)
|
|
345
|
+
|
|
346
|
+
# 知识 & 宪法
|
|
347
|
+
ai-spec learn [lesson] 零摩擦知识注入,直接写入宪法 §9
|
|
348
|
+
|
|
349
|
+
# DSL 衍生产物
|
|
350
|
+
ai-spec export DSL → OpenAPI 3.1.0 YAML/JSON
|
|
351
|
+
ai-spec types DSL → TypeScript 类型文件
|
|
352
|
+
ai-spec mock DSL → Mock 服务器 + MSW Handlers + 代理配置
|
|
353
|
+
|
|
354
|
+
# 配置(无参数运行进入交互式 provider/model 选择)
|
|
355
|
+
ai-spec config 交互式 provider/model 配置(已合并原 model 命令)
|
|
356
|
+
ai-spec config --show 查看当前配置
|
|
357
|
+
ai-spec config --list 列出所有可用 provider 和模型
|
|
358
|
+
|
|
359
|
+
# 可观测性
|
|
360
|
+
ai-spec logs [runId] 查看 run 历史 / 指定 run 的 stage 详情
|
|
361
|
+
ai-spec trend Harness score 趋势分析
|
|
362
|
+
ai-spec dashboard 生成静态 HTML Harness Dashboard
|
|
363
|
+
ai-spec restore <runId> 回滚指定 run 修改的所有文件
|
|
364
|
+
ai-spec vcr list 列出 VCR 录制
|
|
335
365
|
```
|
|
336
366
|
|
|
337
367
|
<details>
|
package/cli/commands/config.ts
CHANGED
|
@@ -2,14 +2,21 @@ import { Command } from "commander";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as fs from "fs-extra";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
5
6
|
import { CodeGenMode } from "../../core/code-generator";
|
|
6
7
|
import { clearAllKeys, clearKey, getSavedKey, KEY_STORE_FILE } from "../../core/key-store";
|
|
7
8
|
import { AiSpecConfig, CONFIG_FILE, loadConfig } from "../utils";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_MODELS,
|
|
11
|
+
ENV_KEY_MAP,
|
|
12
|
+
PROVIDER_CATALOG,
|
|
13
|
+
} from "../../core/spec-generator";
|
|
14
|
+
import { AiSpecGlobalConfig, GLOBAL_CONFIG_FILE, loadGlobalConfig, saveGlobalConfig } from "../utils";
|
|
8
15
|
|
|
9
16
|
export function registerConfig(program: Command): void {
|
|
10
17
|
program
|
|
11
18
|
.command("config")
|
|
12
|
-
.description(`
|
|
19
|
+
.description(`Configure ai-spec: run without flags for interactive model/provider setup`)
|
|
13
20
|
.option("--provider <name>", "Default AI provider for spec generation")
|
|
14
21
|
.option("--model <name>", "Default model for spec generation")
|
|
15
22
|
.option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)")
|
|
@@ -18,7 +25,9 @@ export function registerConfig(program: Command): void {
|
|
|
18
25
|
.option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)")
|
|
19
26
|
.option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)")
|
|
20
27
|
.option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)")
|
|
28
|
+
.option("--max-codegen-concurrency <n>", "Max concurrent tasks per batch in api codegen mode (1-10, default: 3)")
|
|
21
29
|
.option("--show", "Print current configuration")
|
|
30
|
+
.option("--list", "List all available providers and models")
|
|
22
31
|
.option("--reset", "Reset configuration to empty")
|
|
23
32
|
.option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json")
|
|
24
33
|
.option("--clear-key <provider>", "Delete saved API key for a specific provider")
|
|
@@ -27,6 +36,117 @@ export function registerConfig(program: Command): void {
|
|
|
27
36
|
const currentDir = process.cwd();
|
|
28
37
|
const configPath = path.join(currentDir, CONFIG_FILE);
|
|
29
38
|
|
|
39
|
+
// ── --list: show all providers ──────────────────────────────────────────
|
|
40
|
+
if (opts.list) {
|
|
41
|
+
console.log(chalk.bold("\nAvailable providers & models:\n"));
|
|
42
|
+
for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
|
|
43
|
+
console.log(` ${chalk.bold.cyan(key.padEnd(10))} ${chalk.white(meta.displayName)}`);
|
|
44
|
+
console.log(chalk.gray(` ${meta.description}`));
|
|
45
|
+
console.log(chalk.gray(` env: ${meta.envKey} | models: ${meta.models.join(", ")}`));
|
|
46
|
+
console.log();
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── No flags → interactive model/provider picker ────────────────────────
|
|
52
|
+
const anyFlagSet = opts.provider || opts.model || opts.codegen || opts.codegenProvider ||
|
|
53
|
+
opts.codegenModel || opts.minSpecScore !== undefined || opts.minHarnessScore !== undefined ||
|
|
54
|
+
opts.maxErrorCycles !== undefined || opts.maxCodegenConcurrency !== undefined ||
|
|
55
|
+
opts.show || opts.reset || opts.clearKeys || opts.clearKey || opts.listKeys;
|
|
56
|
+
|
|
57
|
+
if (!anyFlagSet) {
|
|
58
|
+
const existing: AiSpecGlobalConfig = await loadGlobalConfig();
|
|
59
|
+
|
|
60
|
+
console.log(chalk.blue("\n─── ai-spec config ─────────────────────────────"));
|
|
61
|
+
console.log(chalk.gray(` Global config: ${GLOBAL_CONFIG_FILE}`));
|
|
62
|
+
if (Object.keys(existing).length > 0) {
|
|
63
|
+
console.log(chalk.gray(
|
|
64
|
+
` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` +
|
|
65
|
+
(existing.codegenProvider ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}` : "")
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
console.log();
|
|
69
|
+
|
|
70
|
+
const target = await select({
|
|
71
|
+
message: "Configure model for:",
|
|
72
|
+
choices: [
|
|
73
|
+
{ name: "Spec generation (spec writing & refinement)", value: "spec" },
|
|
74
|
+
{ name: "Code generation (used when --codegen api is active)", value: "codegen" },
|
|
75
|
+
{ name: "Both (same provider/model for all tasks)", value: "both" },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function pickProviderAndModel(label: string): Promise<{ provider: string; model: string }> {
|
|
80
|
+
const providerKey = await select({
|
|
81
|
+
message: `${label} — select provider:`,
|
|
82
|
+
choices: Object.entries(PROVIDER_CATALOG).map(([key, meta]) => ({
|
|
83
|
+
name: `${meta.displayName.padEnd(22)} ${chalk.gray(meta.description)}`,
|
|
84
|
+
value: key,
|
|
85
|
+
short: meta.displayName,
|
|
86
|
+
})),
|
|
87
|
+
});
|
|
88
|
+
const meta = PROVIDER_CATALOG[providerKey];
|
|
89
|
+
const modelChoices = [
|
|
90
|
+
...meta.models.map((m) => ({ name: m, value: m })),
|
|
91
|
+
{ name: chalk.italic("✎ Enter custom model name..."), value: "__custom__" },
|
|
92
|
+
];
|
|
93
|
+
let chosenModel = await select({
|
|
94
|
+
message: `${label} — select model (${meta.displayName}):`,
|
|
95
|
+
choices: modelChoices,
|
|
96
|
+
});
|
|
97
|
+
if (chosenModel === "__custom__") {
|
|
98
|
+
chosenModel = await input({
|
|
99
|
+
message: "Enter model name:",
|
|
100
|
+
validate: (v) => v.trim().length > 0 || "Model name cannot be empty",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { provider: providerKey, model: chosenModel };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const updated: AiSpecGlobalConfig = { ...existing };
|
|
107
|
+
|
|
108
|
+
if (target === "spec" || target === "both") {
|
|
109
|
+
const { provider, model } = await pickProviderAndModel("Spec");
|
|
110
|
+
updated.provider = provider;
|
|
111
|
+
updated.model = model;
|
|
112
|
+
}
|
|
113
|
+
if (target === "codegen" || target === "both") {
|
|
114
|
+
if (target === "both") {
|
|
115
|
+
updated.codegenProvider = updated.provider;
|
|
116
|
+
updated.codegenModel = updated.model;
|
|
117
|
+
} else {
|
|
118
|
+
const { provider, model } = await pickProviderAndModel("Codegen");
|
|
119
|
+
updated.codegenProvider = provider;
|
|
120
|
+
updated.codegenModel = model;
|
|
121
|
+
}
|
|
122
|
+
const effectiveCodegenProvider = updated.codegenProvider ?? updated.provider ?? "gemini";
|
|
123
|
+
if (effectiveCodegenProvider !== "claude" && updated.codegen === "claude-code") {
|
|
124
|
+
updated.codegen = "api";
|
|
125
|
+
console.log(chalk.yellow(`\n ⚠ provider "${effectiveCodegenProvider}" does not support "claude-code" mode.`));
|
|
126
|
+
console.log(chalk.gray(` codegen mode auto-set to "api".`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(chalk.blue("\n Preview:"));
|
|
131
|
+
console.log(chalk.gray(` spec → ${updated.provider}/${updated.model}`));
|
|
132
|
+
if (updated.codegenProvider) {
|
|
133
|
+
console.log(chalk.gray(` codegen → ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "api"})`));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ok = await confirm({ message: `Save to ${GLOBAL_CONFIG_FILE}?`, default: true });
|
|
137
|
+
if (!ok) { console.log(chalk.gray(" Cancelled.")); return; }
|
|
138
|
+
|
|
139
|
+
await saveGlobalConfig(updated);
|
|
140
|
+
console.log(chalk.green(`\n ✔ Saved to ${GLOBAL_CONFIG_FILE}`));
|
|
141
|
+
|
|
142
|
+
const providerToCheck = updated.provider ?? "gemini";
|
|
143
|
+
const envKey = ENV_KEY_MAP[providerToCheck];
|
|
144
|
+
if (envKey && !process.env[envKey]) {
|
|
145
|
+
console.log(chalk.yellow(` ⚠ Remember to set ${envKey} in your environment or .env file.`));
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
30
150
|
if (opts.clearKeys) {
|
|
31
151
|
await clearAllKeys();
|
|
32
152
|
console.log(chalk.green(`✔ All saved API keys cleared.`));
|
|
@@ -95,6 +215,14 @@ export function registerConfig(program: Command): void {
|
|
|
95
215
|
}
|
|
96
216
|
updated.minHarnessScore = score;
|
|
97
217
|
}
|
|
218
|
+
if (opts.maxCodegenConcurrency !== undefined) {
|
|
219
|
+
const n = parseInt(opts.maxCodegenConcurrency, 10);
|
|
220
|
+
if (isNaN(n) || n < 1 || n > 10) {
|
|
221
|
+
console.error(chalk.red(" --max-codegen-concurrency must be a number between 1 and 10"));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
updated.maxCodegenConcurrency = n;
|
|
225
|
+
}
|
|
98
226
|
if (opts.maxErrorCycles !== undefined) {
|
|
99
227
|
const cycles = parseInt(opts.maxErrorCycles, 10);
|
|
100
228
|
if (isNaN(cycles) || cycles < 1 || cycles > 10) {
|
package/cli/commands/create.ts
CHANGED
|
@@ -205,6 +205,8 @@ export function registerCreate(program: Command): void {
|
|
|
205
205
|
.option("--serve", "After workspace pipeline completes, auto-start mock server + patch frontend proxy")
|
|
206
206
|
.option("--vcr-record", "Record all AI responses to .ai-spec-vcr/ for offline replay")
|
|
207
207
|
.option("--vcr-replay <runId>", "Replay AI responses from a previous recording (zero API calls)")
|
|
208
|
+
.option("--openapi", "Auto-generate OpenAPI 3.1.0 YAML after DSL extraction")
|
|
209
|
+
.option("--types", "Auto-generate TypeScript types after DSL extraction")
|
|
208
210
|
.action(async (idea: string | undefined, opts) => {
|
|
209
211
|
const currentDir = process.cwd();
|
|
210
212
|
const config = await loadConfig(currentDir);
|
|
@@ -237,6 +239,18 @@ export function registerCreate(program: Command): void {
|
|
|
237
239
|
const apiKey = await resolveApiKey(providerName, opts.key as string | undefined);
|
|
238
240
|
const specProvider = createProvider(providerName, apiKey, modelName);
|
|
239
241
|
|
|
242
|
+
// Persist the resolved key on opts so downstream pipelines (single/multi-repo)
|
|
243
|
+
// short-circuit `resolveApiKey` via the cliKey path and skip a duplicate prompt.
|
|
244
|
+
// Without this, the user gets prompted twice for the same provider when
|
|
245
|
+
// create.ts and the pipeline both call resolveApiKey independently.
|
|
246
|
+
if (!opts.key) opts.key = apiKey;
|
|
247
|
+
// If user did NOT specify a separate codegen provider/key, the pipeline will
|
|
248
|
+
// fall back to spec provider — pre-fill codegenKey too so that path is silent.
|
|
249
|
+
const codegenProviderName = (opts.codegenProvider as string) || config.codegenProvider || providerName;
|
|
250
|
+
if (codegenProviderName === providerName && !opts.codegenKey) {
|
|
251
|
+
opts.codegenKey = apiKey;
|
|
252
|
+
}
|
|
253
|
+
|
|
240
254
|
// ── Select repo(s) from registered list ────────────────────────────────
|
|
241
255
|
const registeredRepos = await getRegisteredRepos();
|
|
242
256
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { confirm } from "@inquirer/prompts";
|
|
4
|
+
import {
|
|
5
|
+
loadFixHistory,
|
|
6
|
+
pruneFixHistory,
|
|
7
|
+
aggregateFixPatterns,
|
|
8
|
+
detectPromotionCandidates,
|
|
9
|
+
computeFixHistoryStats,
|
|
10
|
+
FIX_HISTORY_FILE,
|
|
11
|
+
} from "../../core/fix-history";
|
|
12
|
+
import { appendDirectLesson } from "../../core/knowledge-memory";
|
|
13
|
+
import { loadConfig } from "../utils";
|
|
14
|
+
|
|
15
|
+
export function registerFixHistory(program: Command): void {
|
|
16
|
+
program
|
|
17
|
+
.command("fix-history")
|
|
18
|
+
.description("Inspect and manage the import auto-fix history ledger")
|
|
19
|
+
.option("--list", "List raw entries instead of the aggregated summary")
|
|
20
|
+
.option("--prune <days>", "Remove entries older than N days")
|
|
21
|
+
.option("--promote", "Review patterns above threshold and promote to constitution §9")
|
|
22
|
+
.option("--threshold <n>", "Override promotion threshold (default from config, usually 5)")
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
const currentDir = process.cwd();
|
|
25
|
+
const config = await loadConfig(currentDir);
|
|
26
|
+
const history = await loadFixHistory(currentDir);
|
|
27
|
+
|
|
28
|
+
// ── --prune ────────────────────────────────────────────────────────────
|
|
29
|
+
if (opts.prune !== undefined) {
|
|
30
|
+
const days = parseInt(opts.prune, 10);
|
|
31
|
+
if (isNaN(days) || days < 0) {
|
|
32
|
+
console.error(chalk.red(` --prune must be a non-negative integer (days)`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const removed = await pruneFixHistory(currentDir, days);
|
|
36
|
+
if (removed === 0) {
|
|
37
|
+
console.log(chalk.gray(` No entries older than ${days} day(s) to remove.`));
|
|
38
|
+
} else {
|
|
39
|
+
console.log(chalk.green(` ✔ Removed ${removed} entry/entries older than ${days} day(s).`));
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Empty ledger ──────────────────────────────────────────────────────
|
|
45
|
+
if (history.entries.length === 0) {
|
|
46
|
+
console.log(chalk.gray(`\nNo fix history found. Ledger: ${FIX_HISTORY_FILE}`));
|
|
47
|
+
console.log(chalk.gray(` The ledger is populated automatically when import-fixer repairs broken imports.`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── --promote ─────────────────────────────────────────────────────────
|
|
52
|
+
if (opts.promote) {
|
|
53
|
+
const threshold = opts.threshold
|
|
54
|
+
? parseInt(opts.threshold, 10)
|
|
55
|
+
: config.fixHistoryPromotionThreshold ?? 5;
|
|
56
|
+
|
|
57
|
+
const candidates = detectPromotionCandidates(history, threshold);
|
|
58
|
+
if (candidates.length === 0) {
|
|
59
|
+
console.log(chalk.gray(`\n No patterns have crossed the promotion threshold (${threshold}x).`));
|
|
60
|
+
console.log(chalk.gray(` Run the pipeline more to accumulate patterns, or lower the threshold with --threshold <n>.`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(
|
|
65
|
+
chalk.bold(`\n─── Promotion Candidates (threshold: ${threshold}x) ────────────────`)
|
|
66
|
+
);
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.gray(` ${candidates.length} pattern(s) seen at least ${threshold} time(s).`)
|
|
69
|
+
);
|
|
70
|
+
console.log(
|
|
71
|
+
chalk.gray(` Accepted lessons are written to constitution §9 (accumulated lessons).\n`)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
let accepted = 0;
|
|
75
|
+
for (const c of candidates) {
|
|
76
|
+
console.log(
|
|
77
|
+
chalk.cyan(`\n Pattern: ${c.aggregate.source}`) +
|
|
78
|
+
chalk.gray(` (${c.aggregate.count}x, ${c.aggregate.uniqueRunIds} run(s))`)
|
|
79
|
+
);
|
|
80
|
+
console.log(chalk.gray(` Names: { ${c.aggregate.names.join(", ")} }`));
|
|
81
|
+
console.log(chalk.gray(` Reason: ${c.aggregate.reason}`));
|
|
82
|
+
console.log(chalk.gray(` Lesson text:`));
|
|
83
|
+
console.log(chalk.white(` ${c.lessonText}`));
|
|
84
|
+
|
|
85
|
+
const ok = await confirm({
|
|
86
|
+
message: `Promote this pattern to constitution §9?`,
|
|
87
|
+
default: true,
|
|
88
|
+
});
|
|
89
|
+
if (!ok) {
|
|
90
|
+
console.log(chalk.gray(` skipped.`));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const result = await appendDirectLesson(currentDir, c.lessonText);
|
|
94
|
+
if (result.appended) {
|
|
95
|
+
console.log(chalk.green(` ✔ Appended to constitution §9.`));
|
|
96
|
+
accepted++;
|
|
97
|
+
} else {
|
|
98
|
+
console.log(chalk.yellow(` ⚠ Not appended: ${result.reason}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(
|
|
103
|
+
chalk.green(`\n ✔ Promotion complete: ${accepted}/${candidates.length} pattern(s) added to §9.`)
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── --list: raw entries ───────────────────────────────────────────────
|
|
109
|
+
if (opts.list) {
|
|
110
|
+
console.log(chalk.bold(`\n─── Fix History Entries (${history.entries.length}) ────────────────`));
|
|
111
|
+
console.log(chalk.gray(` File: ${FIX_HISTORY_FILE}\n`));
|
|
112
|
+
// Show newest first
|
|
113
|
+
const sorted = [...history.entries].sort((a, b) => b.ts.localeCompare(a.ts));
|
|
114
|
+
for (const e of sorted.slice(0, 50)) {
|
|
115
|
+
const tsShort = e.ts.slice(0, 19).replace("T", " ");
|
|
116
|
+
const stageTag = e.fix.stage === "deterministic" ? chalk.green("[DSL]") : chalk.cyan("[AI ]");
|
|
117
|
+
console.log(
|
|
118
|
+
` ${stageTag} ${chalk.gray(tsShort)} ${chalk.white(e.brokenImport.source)} ${chalk.gray(`{ ${e.brokenImport.names.join(", ")} }`)}`
|
|
119
|
+
);
|
|
120
|
+
console.log(
|
|
121
|
+
chalk.gray(` ${e.fix.kind} → ${e.fix.target} (run: ${e.runId}, ${e.brokenImport.file}:${e.brokenImport.line})`)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (history.entries.length > 50) {
|
|
125
|
+
console.log(chalk.gray(`\n ... ${history.entries.length - 50} older entry(ies) not shown`));
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Default: aggregated summary ───────────────────────────────────────
|
|
131
|
+
const stats = computeFixHistoryStats(history);
|
|
132
|
+
const patterns = aggregateFixPatterns(history);
|
|
133
|
+
|
|
134
|
+
console.log(chalk.bold(`\n─── Fix History Summary ────────────────────────────`));
|
|
135
|
+
console.log(chalk.gray(` File: ${FIX_HISTORY_FILE}\n`));
|
|
136
|
+
console.log(` Total fixes applied : ${chalk.white(String(stats.totalEntries))}`);
|
|
137
|
+
console.log(` Unique patterns : ${chalk.white(String(stats.uniquePatterns))}`);
|
|
138
|
+
console.log(` Runs that triggered : ${chalk.white(String(stats.uniqueRunIds))}`);
|
|
139
|
+
console.log(
|
|
140
|
+
` Stage A (deterministic): ${chalk.green(String(stats.byStage.deterministic))} · Stage B (AI): ${chalk.cyan(String(stats.byStage.ai))}`
|
|
141
|
+
);
|
|
142
|
+
console.log(
|
|
143
|
+
` Reasons : file_not_found ${stats.byReason.file_not_found} · missing_export ${stats.byReason.missing_export}`
|
|
144
|
+
);
|
|
145
|
+
if (stats.lastEntryTs) {
|
|
146
|
+
console.log(` Last fix : ${chalk.gray(stats.lastEntryTs.slice(0, 19).replace("T", " "))}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(chalk.bold(`\n Top patterns (by frequency):`));
|
|
150
|
+
const top = patterns.slice(0, 10);
|
|
151
|
+
for (const p of top) {
|
|
152
|
+
const stageTag = p.fix.stage === "deterministic" ? chalk.green("[DSL]") : chalk.cyan("[AI ]");
|
|
153
|
+
const countColor = p.count >= 5 ? chalk.red : p.count >= 3 ? chalk.yellow : chalk.gray;
|
|
154
|
+
console.log(
|
|
155
|
+
` ${stageTag} ${countColor(`${p.count}x`.padStart(4))} ${chalk.white(p.source)} ${chalk.gray(`{ ${p.names.join(", ")} }`)}`
|
|
156
|
+
);
|
|
157
|
+
console.log(chalk.gray(` last seen: ${p.lastSeen.slice(0, 10)} · ${p.uniqueRunIds} run(s)`));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const promotionThreshold = config.fixHistoryPromotionThreshold ?? 5;
|
|
161
|
+
const promotable = patterns.filter((p) => p.count >= promotionThreshold).length;
|
|
162
|
+
if (promotable > 0) {
|
|
163
|
+
console.log(
|
|
164
|
+
chalk.yellow(
|
|
165
|
+
`\n ⚠ ${promotable} pattern(s) have crossed the promotion threshold (${promotionThreshold}x). ` +
|
|
166
|
+
`Run \`ai-spec fix-history --promote\` to review.`
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(chalk.gray(`\n Hints:`));
|
|
172
|
+
console.log(chalk.gray(` ai-spec fix-history --list Show raw entries (newest first)`));
|
|
173
|
+
console.log(chalk.gray(` ai-spec fix-history --promote Promote repeated patterns to constitution §9`));
|
|
174
|
+
console.log(chalk.gray(` ai-spec fix-history --prune 30 Remove entries older than 30 days`));
|
|
175
|
+
});
|
|
176
|
+
}
|
package/cli/commands/init.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
buildGlobalConstitutionPrompt,
|
|
19
19
|
} from "../../prompts/global-constitution.prompt";
|
|
20
20
|
import { loadConfig, resolveApiKey } from "../utils";
|
|
21
|
-
import { loadIndex, ProjectEntry } from "../../core/project-index";
|
|
21
|
+
import { loadIndex, runScan, saveIndex, ProjectEntry } from "../../core/project-index";
|
|
22
22
|
import { detectRepoType } from "../../core/workspace-loader";
|
|
23
23
|
import {
|
|
24
24
|
RegisteredRepo,
|
|
@@ -224,8 +224,30 @@ export function registerInit(program: Command): void {
|
|
|
224
224
|
.option("--consolidate", "Consolidate §9 accumulated lessons into §1–§8 core rules")
|
|
225
225
|
.option("--dry-run", "Preview consolidation result without writing (use with --consolidate)")
|
|
226
226
|
.option("--add-repo", "Add a new repo to the registered list")
|
|
227
|
+
.option("--status", "Show registered repos and constitution status (no changes made)")
|
|
227
228
|
.action(async (opts) => {
|
|
228
229
|
const currentDir = process.cwd();
|
|
230
|
+
|
|
231
|
+
// ── --status: show registered repos and constitution health ───────────
|
|
232
|
+
if (opts.status) {
|
|
233
|
+
const repos = await getRegisteredRepos();
|
|
234
|
+
if (repos.length === 0) {
|
|
235
|
+
console.log(chalk.yellow("\nNo repos registered. Run `ai-spec init` to add repos."));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.log(chalk.bold(`\n─── Registered Repos (${repos.length}) ──────────────────────`));
|
|
239
|
+
for (const r of repos) {
|
|
240
|
+
const constitutionIcon = r.hasConstitution ? chalk.green("✔ §C") : chalk.gray("○ §C");
|
|
241
|
+
const roleColor = r.role === "frontend" ? chalk.green : r.role === "backend" ? chalk.blue : r.role === "mobile" ? chalk.magenta : chalk.gray;
|
|
242
|
+
const pathExists = await fs.pathExists(r.path);
|
|
243
|
+
const pathStatus = pathExists ? chalk.gray(r.path) : chalk.red(`${r.path} (not found)`);
|
|
244
|
+
console.log(` ${constitutionIcon} ${roleColor(r.role.padEnd(9))} ${chalk.white(r.name.padEnd(20))} ${pathStatus}`);
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk.gray(`\n Store: ${REPO_STORE_FILE}`));
|
|
247
|
+
console.log(chalk.gray(" Run `ai-spec init` to add repos or regenerate constitutions."));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
229
251
|
const config = await loadConfig(currentDir);
|
|
230
252
|
|
|
231
253
|
const providerName = opts.provider || config.provider || "gemini";
|
|
@@ -388,6 +410,19 @@ export function registerInit(program: Command): void {
|
|
|
388
410
|
}
|
|
389
411
|
console.log(chalk.gray(`\n Repo store: ${REPO_STORE_FILE}`));
|
|
390
412
|
console.log(chalk.gray(` Next step: ai-spec create "your feature idea"`));
|
|
413
|
+
|
|
414
|
+
// ── Auto-scan: silently update project index ───────────────────────────
|
|
415
|
+
try {
|
|
416
|
+
const { index, added, updated: upd, nowMissing } = await runScan(currentDir, 2);
|
|
417
|
+
await saveIndex(currentDir, index);
|
|
418
|
+
const changes = added.length + upd.length + nowMissing.length;
|
|
419
|
+
if (changes > 0) {
|
|
420
|
+
console.log(chalk.gray(` Project index updated (${index.projects.filter((p: ProjectEntry) => !p.missing).length} projects found).`));
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// scan failure is non-blocking
|
|
424
|
+
}
|
|
425
|
+
|
|
391
426
|
process.exit(0);
|
|
392
427
|
});
|
|
393
428
|
}
|
package/cli/index.ts
CHANGED
|
@@ -8,8 +8,6 @@ import { registerCreate } from "./commands/create";
|
|
|
8
8
|
import { registerReview } from "./commands/review";
|
|
9
9
|
import { registerInit } from "./commands/init";
|
|
10
10
|
import { registerConfig } from "./commands/config";
|
|
11
|
-
import { registerModel } from "./commands/model";
|
|
12
|
-
import { registerWorkspace } from "./commands/workspace";
|
|
13
11
|
import { registerUpdate } from "./commands/update";
|
|
14
12
|
import { registerExport } from "./commands/export";
|
|
15
13
|
import { registerMock } from "./commands/mock";
|
|
@@ -20,7 +18,7 @@ import { registerLogs } from "./commands/logs";
|
|
|
20
18
|
import { registerTypes } from "./commands/types";
|
|
21
19
|
import { registerDashboard } from "./commands/dashboard";
|
|
22
20
|
import { registerVcr } from "./commands/vcr";
|
|
23
|
-
import {
|
|
21
|
+
import { registerFixHistory } from "./commands/fix-history";
|
|
24
22
|
|
|
25
23
|
const program = new Command();
|
|
26
24
|
|
|
@@ -33,8 +31,6 @@ registerCreate(program);
|
|
|
33
31
|
registerReview(program);
|
|
34
32
|
registerInit(program);
|
|
35
33
|
registerConfig(program);
|
|
36
|
-
registerModel(program);
|
|
37
|
-
registerWorkspace(program);
|
|
38
34
|
registerUpdate(program);
|
|
39
35
|
registerExport(program);
|
|
40
36
|
registerMock(program);
|
|
@@ -45,6 +41,6 @@ registerLogs(program);
|
|
|
45
41
|
registerTypes(program);
|
|
46
42
|
registerDashboard(program);
|
|
47
43
|
registerVcr(program);
|
|
48
|
-
|
|
44
|
+
registerFixHistory(program);
|
|
49
45
|
|
|
50
46
|
program.parse();
|
package/cli/pipeline/helpers.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type MultiRepoResult = {
|
|
|
10
10
|
dsl: SpecDSL | null;
|
|
11
11
|
repoAbsPath: string;
|
|
12
12
|
role: string;
|
|
13
|
+
/** Files written by codegen during this run. Empty when codegen failed or never ran. */
|
|
14
|
+
generatedFiles: string[];
|
|
15
|
+
/** Human-readable reason when status === "failed". */
|
|
16
|
+
failureReason?: string;
|
|
17
|
+
/** Per-repo run ID (when RunLogger was created for this repo). */
|
|
18
|
+
runId?: string;
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
// ─── Banner ──────────────────────────────────────────────────────────────────
|