ai-spec-dev 0.30.1 → 0.33.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 +5 -1
- package/README.md +29 -1
- package/RELEASE_LOG.md +188 -0
- package/cli/commands/config.ts +93 -0
- package/cli/commands/export.ts +66 -0
- package/cli/commands/init.ts +153 -0
- package/cli/commands/learn.ts +30 -0
- package/cli/commands/logs.ts +106 -0
- package/cli/commands/model.ts +156 -0
- package/cli/commands/restore.ts +22 -0
- package/cli/commands/review.ts +63 -0
- package/cli/commands/trend.ts +36 -0
- package/cli/commands/update.ts +178 -0
- package/cli/commands/workspace.ts +219 -0
- package/cli/index.ts +301 -1
- package/cli/utils.ts +83 -0
- package/core/dsl-feedback.ts +255 -0
- package/core/prompt-hasher.ts +42 -0
- package/core/run-logger.ts +21 -0
- package/core/run-trend.ts +241 -0
- package/core/self-evaluator.ts +276 -0
- package/dist/cli/index.js +1089 -445
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1089 -445
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
- package/purpose.md +189 -2
- package/tests/dsl-extractor.test.ts +264 -0
- package/tests/dsl-feedback.test.ts +266 -0
- package/tests/dsl-validator.test.ts +283 -0
- package/tests/error-feedback.test.ts +292 -0
- package/tests/provider-utils.test.ts +173 -0
- package/tests/run-trend.test.ts +186 -0
- package/tests/self-evaluator.test.ts +339 -0
- package/tests/spec-assessor.test.ts +142 -0
- package/tests/task-generator.test.ts +230 -0
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
"Bash(npm run:*)",
|
|
5
5
|
"Bash(python3 -c \":*)",
|
|
6
6
|
"Bash(grep -E \"^./\\\\w+/|^./\\\\w+\\\\.ts$\")",
|
|
7
|
-
"Bash(wc -l core/*.ts prompts/*.ts)"
|
|
7
|
+
"Bash(wc -l core/*.ts prompts/*.ts)",
|
|
8
|
+
"Bash(npx tsc:*)",
|
|
9
|
+
"Bash(npm install:*)",
|
|
10
|
+
"Bash(npx vitest:*)",
|
|
11
|
+
"Bash(npm test:*)"
|
|
8
12
|
]
|
|
9
13
|
}
|
|
10
14
|
}
|
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ ai-spec create "给用户模块增加登录功能"
|
|
|
93
93
|
[cycle 2/2] Running tests: npm test
|
|
94
94
|
✔ Tests passed.
|
|
95
95
|
✔ All checks passed after 2 cycle(s).
|
|
96
|
-
[9/
|
|
96
|
+
[9/10] Automated code review (3-pass: architecture + implementation + impact/complexity)...
|
|
97
97
|
Pass 1/3: Architecture review...
|
|
98
98
|
Pass 2/3: Implementation review...
|
|
99
99
|
Pass 3/3: Impact & complexity assessment...
|
|
@@ -1259,6 +1259,32 @@ Attempting auto-fix (3 error(s))...
|
|
|
1259
1259
|
|
|
1260
1260
|
---
|
|
1261
1261
|
|
|
1262
|
+
### Step 10 — Harness Self-Eval
|
|
1263
|
+
|
|
1264
|
+
代码审查完成后自动执行,**零 AI 调用**,纯确定性评分:
|
|
1265
|
+
|
|
1266
|
+
| 维度 | 评分逻辑 |
|
|
1267
|
+
|------|---------|
|
|
1268
|
+
| DSL Coverage (0-10) | 生成文件是否覆盖 DSL 声明的 endpoint 层和 model 层 |
|
|
1269
|
+
| Compile Score (0-10) | error feedback 全通过 → 10;未通过 / 跳过 → 5 |
|
|
1270
|
+
| Review Score (0-10) | 从 3-pass review 文本提取 `Score: X/10` |
|
|
1271
|
+
|
|
1272
|
+
**Harness Score** = 加权平均(DSL 40% + Compile 30% + Review 30%)
|
|
1273
|
+
|
|
1274
|
+
```
|
|
1275
|
+
─── Harness Self-Eval ───────────────────────────
|
|
1276
|
+
Score : [████████░░] 7.8/10
|
|
1277
|
+
DSL : 8/10 Compile: pass Review: 7.2/10
|
|
1278
|
+
Prompt : a3f2c1d8
|
|
1279
|
+
─────────────────────────────────────────────────
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
- `harnessScore` 和 `promptHash` 写入 RunLog(`.ai-spec-logs/<runId>.json`)
|
|
1283
|
+
- 每次改动 prompt 文件后,`promptHash` 自动变化,结合 `harnessScore` 可量化 prompt 改动的效果
|
|
1284
|
+
- 后续可通过脚本聚合多个 RunLog,绘制 harnessScore × promptHash 的趋势图
|
|
1285
|
+
|
|
1286
|
+
---
|
|
1287
|
+
|
|
1262
1288
|
## 多 Repo 工作区模式
|
|
1263
1289
|
|
|
1264
1290
|
当父目录中存在 `.ai-spec-workspace.json` 时,`ai-spec create` 自动切换为**多 Repo 联动模式**,一句需求驱动前后端全链路实现。
|
|
@@ -1525,6 +1551,8 @@ ai-spec-dev-poc/
|
|
|
1525
1551
|
│ ├── reviewer.ts # AI 代码审查(git diff / 文件内容双模式)
|
|
1526
1552
|
│ ├── test-generator.ts # 测试骨架生成器(DSL → Jest/Vitest 骨架)
|
|
1527
1553
|
│ ├── error-feedback.ts # 错误反馈自动修复(测试+lint检测 · 依赖图排序修复 · AI修复循环)
|
|
1554
|
+
│ ├── prompt-hasher.ts # [v0.31.0] Prompt Hash:6 个核心 prompt 的 SHA-256 短 hash
|
|
1555
|
+
│ ├── self-evaluator.ts # [v0.31.0] Harness Self-Eval:零 AI 调用,DSL覆盖+编译+review加权评分
|
|
1528
1556
|
│ ├── knowledge-memory.ts # 经验积累:审查 issue → 宪法§9
|
|
1529
1557
|
│ ├── workspace-loader.ts # [Phase 4] 工作区配置加载 + repo 类型自动检测
|
|
1530
1558
|
│ ├── requirement-decomposer.ts # [Phase 4] 需求跨 repo 拆分 + UX 决策生成
|
package/RELEASE_LOG.md
CHANGED
|
@@ -2,6 +2,194 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## [0.33.0] 2026-03-30 — Pipeline 反馈环:DSL Gap Loop + Review→DSL Loop
|
|
6
|
+
|
|
7
|
+
### 新增内容
|
|
8
|
+
|
|
9
|
+
**Feature — 两条 Pipeline 反馈环(`core/dsl-feedback.ts`、`cli/index.ts`)**
|
|
10
|
+
|
|
11
|
+
原有流水线是严格单向的——每一步的输出只能向前传递,review 发现的问题只能写入 §9,DSL 提取稀疏也只能硬着头皮继续。v0.33.0 在两个关键位置插入局部反馈环,让 pipeline 在保持可测量性的前提下具备弹性。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
**Loop 1 — DSL Gap Feedback(DSL 提取完成 → Worktree 之前)**
|
|
16
|
+
|
|
17
|
+
- 新增 `assessDslRichness(dsl)` — 纯启发式检查,零 AI 调用,检测四类常见 DSL 缺口:
|
|
18
|
+
|
|
19
|
+
| 缺口类型 | 检测逻辑 |
|
|
20
|
+
|----------|---------|
|
|
21
|
+
| `no_models_no_endpoints` | DSL 完全为空——spec 可能太抽象 |
|
|
22
|
+
| `generic_endpoint_desc` | endpoint description < 15 字符或以模糊动词开头(handles/管理/处理…)|
|
|
23
|
+
| `missing_errors` | ≥2 个 endpoint 且全部无 errors 定义 |
|
|
24
|
+
| `sparse_model` | model 字段数 < 2 |
|
|
25
|
+
|
|
26
|
+
- 发现缺口时,交互模式下展示具体缺口列表并提供选择:
|
|
27
|
+
- `🔧 Refine spec` — AI 执行定向 spec 补全(`buildDslGapRefinementPrompt`),不改变功能范围,只填充缺失细节 → 自动重新提取 DSL
|
|
28
|
+
- `⏭ Skip` — 继续用当前 DSL
|
|
29
|
+
|
|
30
|
+
- `--auto` / `--fast` / `--skip-dsl` 模式下完全跳过此 Loop
|
|
31
|
+
- 结果写入 RunLog `dsl_gap_feedback` 阶段(action: `refined` / `skipped` / `refinement_error`)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
**Loop 2 — Review → DSL Structural Feedback(§9 知识积累 → Self-Eval 之前)**
|
|
36
|
+
|
|
37
|
+
- 新增 `extractStructuralFindings(reviewText)` — 解析 Pass 1(架构审查)文本,识别设计层问题(而非实现层问题):
|
|
38
|
+
|
|
39
|
+
| 类别 | 触发模式 |
|
|
40
|
+
|------|---------|
|
|
41
|
+
| `auth_design` | 缺少认证 / missing auth / 鉴权缺 |
|
|
42
|
+
| `api_contract` | 接口设计问题 / API design / 接口缺少 |
|
|
43
|
+
| `model_design` | 模型缺少字段 / model missing field / schema incomplete |
|
|
44
|
+
| `layer_violation` | 层级违反 / layer violation / 分层问题 |
|
|
45
|
+
|
|
46
|
+
Pass 1 得分 ≥ 8 时认为架构没问题,自动跳过分类
|
|
47
|
+
|
|
48
|
+
- 发现结构性问题时展示区别于 §9 的"设计层警告",并提供三种选择:
|
|
49
|
+
- `🔧 Amend spec + update DSL` — AI 根据结构性发现定向修订 spec → 重新提取 DSL → 覆盖保存 spec 文件和 DSL 文件 → 提示 `ai-spec update --codegen` 重新生成受影响文件
|
|
50
|
+
- `📝 Note in §9 only` — §9 已由 knowledge accumulation 写入,DSL 不变
|
|
51
|
+
- `⏭ Skip`
|
|
52
|
+
|
|
53
|
+
- 关键设计决策:Loop 2 **不自动触发 codegen**。DSL 修正后提示用户主动运行 `update --codegen`,保持人在决策节点的控制权
|
|
54
|
+
- `--auto` 模式下完全跳过此 Loop(不增加 CI 耗时)
|
|
55
|
+
- 结果写入 RunLog `review_dsl_feedback` 阶段
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
**新流水线结构:**
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Spec → DSL 提取
|
|
63
|
+
↓
|
|
64
|
+
[Loop 1] DSL Gap 检测
|
|
65
|
+
↓ (不满足 → 定向 spec 补全 → 重新提取 DSL)
|
|
66
|
+
Approval Gate → Worktree → Codegen → ErrorFix → Review
|
|
67
|
+
↓
|
|
68
|
+
§9 知识积累
|
|
69
|
+
↓
|
|
70
|
+
[Loop 2] 结构性问题检测
|
|
71
|
+
↓ (发现 → spec 修订 → DSL 更新)
|
|
72
|
+
Self-Eval → Done
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## [0.32.0] 2026-03-30 — Harness 数据闭环:`trend` / `logs` 命令 + DSL Coverage 细化评分
|
|
78
|
+
|
|
79
|
+
### 新增内容
|
|
80
|
+
|
|
81
|
+
**Feature #1 — `ai-spec trend` 跨运行趋势命令(`core/run-trend.ts`、`cli/index.ts`)**
|
|
82
|
+
|
|
83
|
+
- 新增 `core/run-trend.ts` — 趋势分析模块:
|
|
84
|
+
- `loadRunLogs(workingDir)` — 扫描 `.ai-spec-logs/*.json`,按运行时间倒序排列,静默跳过损坏文件
|
|
85
|
+
- `buildTrendReport(logs, opts)` — 从 RunLog 数组生成趋势报告:按 `promptHash` 分组,统计 avg / best / worst;支持 `last` 和 `promptFilter` 选项
|
|
86
|
+
- `printTrendReport(report, workingDir)` — 彩色表格输出,分为「Prompt 版本摘要」和「运行历史」两区,当前 prompt 版本用 `◀ current` 标记
|
|
87
|
+
- 新增 CLI 命令 `ai-spec trend`:
|
|
88
|
+
```bash
|
|
89
|
+
ai-spec trend # 最近 15 次有评分的运行,按 promptHash 分组
|
|
90
|
+
ai-spec trend --last 30 # 最近 30 次
|
|
91
|
+
ai-spec trend --prompt a3f # 只看 hash 以 a3f 开头的 prompt 版本
|
|
92
|
+
ai-spec trend --json # 输出原始 JSON,适合脚本聚合分析
|
|
93
|
+
```
|
|
94
|
+
输出示例:
|
|
95
|
+
```
|
|
96
|
+
─── Harness Trend ───────────────────────────────────────────────
|
|
97
|
+
Prompt Versions:
|
|
98
|
+
Hash Runs Avg Best Worst Last seen
|
|
99
|
+
─────────────────────────────────────────────────────────
|
|
100
|
+
a3f2c1d8 3 7.6 8.2 6.9 2026-03-30 ◀ current
|
|
101
|
+
b1e4a2f0 5 6.8 7.4 5.5 2026-03-29
|
|
102
|
+
|
|
103
|
+
Run History:
|
|
104
|
+
2026-03-30 [████████░░] 7.8 a3f2c1d8 1m24s feature-login-v1.md
|
|
105
|
+
2026-03-30 [████████░░] 8.2 a3f2c1d8 1m18s feature-user-v1.md
|
|
106
|
+
...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Feature #2 — `ai-spec logs` 运行日志列表命令(`cli/index.ts`)**
|
|
110
|
+
|
|
111
|
+
- 新增 CLI 命令 `ai-spec logs`:
|
|
112
|
+
```bash
|
|
113
|
+
ai-spec logs # 列出最近 10 次运行(runId、日期、score、文件数、耗时)
|
|
114
|
+
ai-spec logs --last 20 # 列出最近 20 次
|
|
115
|
+
ai-spec logs <runId> # 展示该次运行的完整阶段耗时表格
|
|
116
|
+
```
|
|
117
|
+
单次运行详情示例:
|
|
118
|
+
```
|
|
119
|
+
─── Run: 20260330-143022-a7f2 ─────────────────────────────────
|
|
120
|
+
Started : 2026-03-30T14:30:22.000Z
|
|
121
|
+
Provider: gemini / gemini-2.5-pro
|
|
122
|
+
Prompt : a3f2c1d8
|
|
123
|
+
Score : 7.8/10
|
|
124
|
+
Stages:
|
|
125
|
+
✔ context_load 0.3s
|
|
126
|
+
✔ spec_gen 18.4s
|
|
127
|
+
✔ dsl_extract 6.1s
|
|
128
|
+
✔ codegen 51.2s
|
|
129
|
+
✔ review 14.8s
|
|
130
|
+
✔ self_eval 0.0s
|
|
131
|
+
```
|
|
132
|
+
- 结尾提示 `ai-spec logs <runId>` 和 `ai-spec trend`,引导用户进入分析工作流
|
|
133
|
+
|
|
134
|
+
### 功能增强
|
|
135
|
+
|
|
136
|
+
**Enhancement — DSL Coverage Score 三层细化评分(`core/self-evaluator.ts`)**
|
|
137
|
+
|
|
138
|
+
原有评分只做二元判断(endpoint 层有无、model 层有无),无法反映实际覆盖深度。新增两个 Tier:
|
|
139
|
+
|
|
140
|
+
| Tier | 检查项 | 扣分规则 |
|
|
141
|
+
|------|--------|--------|
|
|
142
|
+
| Tier 1(原有)| endpoint 层存在 / model 层存在 | -4 / -3(同前)|
|
|
143
|
+
| **Tier 2(新增)** | Model name 覆盖率:对每个 DSL 声明的 model,检查其名称(含 camelCase → snake_case 规范化)是否出现在任何生成文件路径中 | coverage < 50% → -2;50-79% → -1;≥80% → 0 |
|
|
144
|
+
| **Tier 3(新增)** | Endpoint 文件充足性:≥5 个端点但 endpoint 层文件 < 2 个 | -1 |
|
|
145
|
+
|
|
146
|
+
- 新增 `modelNameTokens(name)` — 将 PascalCase 模型名规范化为多种路径匹配 token(`OrderItem` → `orderitem` / `order-item` / `order_item`)
|
|
147
|
+
- `SelfEvalResult.detail` 新增 `endpointLayerFiles`、`modelNameCoverage`、`modelNameMatched` 字段,写入 RunLog 供后续分析
|
|
148
|
+
- `printSelfEval()` 当存在 DSL model 时新增 Detail 行:
|
|
149
|
+
```
|
|
150
|
+
─── Harness Self-Eval ───────────────────────────
|
|
151
|
+
Score : [████████░░] 7.8/10
|
|
152
|
+
DSL : 8/10 Compile: pass Review: 7.2/10
|
|
153
|
+
Detail : Models: 3/4 (75%) Endpoints: 5 Files: 9
|
|
154
|
+
Prompt : a3f2c1d8
|
|
155
|
+
─────────────────────────────────────────────────
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## [0.31.0] 2026-03-29 — Harness Engineer:Prompt Hash + Create 内联 Self-Eval
|
|
161
|
+
|
|
162
|
+
### 新增内容
|
|
163
|
+
|
|
164
|
+
**Feature #1 — Prompt Hash 关联(`core/prompt-hasher.ts`、`core/run-logger.ts`)**
|
|
165
|
+
|
|
166
|
+
- 新增 `computePromptHash()` — 对 6 个核心 prompt 字符串(codegen、DSL extractor、spec generator、review 三 pass)计算 SHA-256 并取前 8 位,返回形如 `a3f2c1d8` 的短 hex 字符串
|
|
167
|
+
- `RunLog` 新增 `promptHash?: string` 字段;`RunLogger` 新增 `setPromptHash()` + `setHarnessScore()` 方法
|
|
168
|
+
- `ai-spec create` 运行开始时立即调用 `computePromptHash()` 写入 RunLog,任何 prompt 文件改动都会产生不同的 hash
|
|
169
|
+
- **目的**:跨多次运行对比 `harnessScore` 时,可以精确知道「这两次用的 prompt 版本是否相同」,将 prompt 改动的效果从模型随机性中解耦
|
|
170
|
+
|
|
171
|
+
**Feature #2 — Create 内联 Harness Self-Eval(`core/self-evaluator.ts`、`cli/index.ts`)**
|
|
172
|
+
|
|
173
|
+
- 新增 `core/self-evaluator.ts` — 零 AI 调用的确定性评分模块:
|
|
174
|
+
- **DSL Coverage Score (0-10)**:检查 `generatedFiles` 中是否存在 endpoint 层文件(`src/api*`、`src/routes*`、`src/controller*`…)和 model 层文件(`src/model*`、`prisma/`、`src/db*`…),与 DSL 中声明的 endpoint / model 数量对照
|
|
175
|
+
- **Compile Score (0-10)**:`runErrorFeedback()` 返回 `true` → 10,未通过 / 跳过 → 5
|
|
176
|
+
- **Review Score (0-10)**:从 3-pass review 文本中提取 `Score: X/10`(与 `reviewer.ts` 同规则),review 跳过时为 null
|
|
177
|
+
- **Harness Score**:加权平均(有 review:DSL×40% + Compile×30% + Review×30%;无 review:DSL×55% + Compile×45%)
|
|
178
|
+
- `runErrorFeedback()` 的返回值(`boolean`)现在被接住赋给 `compilePassed`,传入 self-eval
|
|
179
|
+
- `ai-spec create` Step 9(code review)之后新增 **Step 10: Harness Self-Eval**,完成后打印:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
─── Harness Self-Eval ───────────────────────────
|
|
183
|
+
Score : [████████░░] 7.8/10
|
|
184
|
+
DSL : 8/10 Compile: pass Review: 7.2/10
|
|
185
|
+
Prompt : a3f2c1d8
|
|
186
|
+
─────────────────────────────────────────────────
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- `harnessScore` 和所有维度分数写入 RunLog 的 `self_eval:done` 事件 + 根级 `harnessScore` 字段,便于后续脚本聚合分析
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
5
193
|
## [0.30.0] 2026-03-29 — 错误修复依赖图排序 + 前端 Import 多行感知解析
|
|
6
194
|
|
|
7
195
|
### 改进内容
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs-extra";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { CodeGenMode } from "../../core/code-generator";
|
|
6
|
+
import { clearAllKeys, clearKey, getSavedKey, KEY_STORE_FILE } from "../../core/key-store";
|
|
7
|
+
import { AiSpecConfig, CONFIG_FILE, loadConfig } from "../utils";
|
|
8
|
+
|
|
9
|
+
export function registerConfig(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command("config")
|
|
12
|
+
.description(`Set default configuration for this project (saved to ${CONFIG_FILE})`)
|
|
13
|
+
.option("--provider <name>", "Default AI provider for spec generation")
|
|
14
|
+
.option("--model <name>", "Default model for spec generation")
|
|
15
|
+
.option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)")
|
|
16
|
+
.option("--codegen-provider <name>", "Default provider for code generation")
|
|
17
|
+
.option("--codegen-model <name>", "Default model for code generation")
|
|
18
|
+
.option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)")
|
|
19
|
+
.option("--show", "Print current configuration")
|
|
20
|
+
.option("--reset", "Reset configuration to empty")
|
|
21
|
+
.option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json")
|
|
22
|
+
.option("--clear-key <provider>", "Delete saved API key for a specific provider")
|
|
23
|
+
.option("--list-keys", "Show which providers have a saved key")
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
const currentDir = process.cwd();
|
|
26
|
+
const configPath = path.join(currentDir, CONFIG_FILE);
|
|
27
|
+
|
|
28
|
+
if (opts.clearKeys) {
|
|
29
|
+
await clearAllKeys();
|
|
30
|
+
console.log(chalk.green(`✔ All saved API keys cleared.`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (opts.clearKey) {
|
|
35
|
+
await clearKey(opts.clearKey);
|
|
36
|
+
console.log(chalk.green(`✔ Saved key for "${opts.clearKey}" removed.`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (opts.listKeys) {
|
|
41
|
+
const store: Record<string, string> = await fs.readJson(KEY_STORE_FILE).catch(() => ({}));
|
|
42
|
+
const providers = Object.keys(store);
|
|
43
|
+
if (providers.length === 0) {
|
|
44
|
+
console.log(chalk.gray("No saved API keys."));
|
|
45
|
+
} else {
|
|
46
|
+
console.log(chalk.bold("Saved API keys:"));
|
|
47
|
+
for (const p of providers) {
|
|
48
|
+
const k = store[p];
|
|
49
|
+
console.log(chalk.gray(` ${p}: ${k.slice(0, 6)}...${k.slice(-4)}`));
|
|
50
|
+
}
|
|
51
|
+
console.log(chalk.gray(`\nFile: ${KEY_STORE_FILE}`));
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.reset) {
|
|
57
|
+
await fs.writeJson(configPath, {}, { spaces: 2 });
|
|
58
|
+
console.log(chalk.green(`✔ Config reset: ${configPath}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const existing: AiSpecConfig = await loadConfig(currentDir);
|
|
63
|
+
|
|
64
|
+
if (opts.show) {
|
|
65
|
+
if (Object.keys(existing).length === 0) {
|
|
66
|
+
console.log(chalk.gray("No config file found. Using built-in defaults."));
|
|
67
|
+
} else {
|
|
68
|
+
console.log(chalk.bold(`${configPath}:`));
|
|
69
|
+
console.log(JSON.stringify(existing, null, 2));
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const updated: AiSpecConfig = { ...existing };
|
|
75
|
+
if (opts.provider) updated.provider = opts.provider;
|
|
76
|
+
if (opts.model) updated.model = opts.model;
|
|
77
|
+
if (opts.codegen) updated.codegen = opts.codegen as CodeGenMode;
|
|
78
|
+
if (opts.codegenProvider) updated.codegenProvider = opts.codegenProvider;
|
|
79
|
+
if (opts.codegenModel) updated.codegenModel = opts.codegenModel;
|
|
80
|
+
if (opts.minSpecScore !== undefined) {
|
|
81
|
+
const score = parseInt(opts.minSpecScore, 10);
|
|
82
|
+
if (isNaN(score) || score < 0 || score > 10) {
|
|
83
|
+
console.error(chalk.red(" --min-spec-score must be a number between 0 and 10"));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
updated.minSpecScore = score;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await fs.writeJson(configPath, updated, { spaces: 2 });
|
|
90
|
+
console.log(chalk.green(`✔ Config saved to ${configPath}`));
|
|
91
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs-extra";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { SpecDSL } from "../../core/dsl-types";
|
|
6
|
+
import { exportOpenApi } from "../../core/openapi-exporter";
|
|
7
|
+
import { findLatestDslFile } from "../../core/mock-server-generator";
|
|
8
|
+
|
|
9
|
+
export function registerExport(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command("export")
|
|
12
|
+
.description("Export the latest DSL to OpenAPI 3.1.0 (YAML or JSON)")
|
|
13
|
+
.option("--openapi", "Export as OpenAPI 3.1.0 (default behaviour)")
|
|
14
|
+
.option("--format <fmt>", "Output format: yaml | json (default: yaml)", "yaml")
|
|
15
|
+
.option("--output <path>", "Output file path (default: openapi.yaml)")
|
|
16
|
+
.option("--server <url>", "API server URL in the OpenAPI document (default: http://localhost:3000)")
|
|
17
|
+
.option("--dsl <path>", "Path to a specific .dsl.json file (auto-detected if omitted)")
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const currentDir = process.cwd();
|
|
20
|
+
|
|
21
|
+
// ── Find DSL ────────────────────────────────────────────────────────────
|
|
22
|
+
let dslPath: string | null = opts.dsl ?? null;
|
|
23
|
+
if (!dslPath) {
|
|
24
|
+
dslPath = await findLatestDslFile(currentDir);
|
|
25
|
+
if (!dslPath) {
|
|
26
|
+
console.error(chalk.red(" No .dsl.json file found. Run `ai-spec create` first or use --dsl <path>."));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
console.log(chalk.gray(` Using DSL: ${path.relative(currentDir, dslPath)}`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let dsl: SpecDSL;
|
|
33
|
+
try {
|
|
34
|
+
dsl = await fs.readJson(dslPath);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(chalk.red(` Failed to read DSL: ${(err as Error).message}`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Export ──────────────────────────────────────────────────────────────
|
|
41
|
+
console.log(chalk.blue("\n─── ai-spec export ─────────────────────────────"));
|
|
42
|
+
|
|
43
|
+
const format = (opts.format === "json" ? "json" : "yaml") as "yaml" | "json";
|
|
44
|
+
const serverUrl = opts.server || "http://localhost:3000";
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const outputPath = await exportOpenApi(dsl, currentDir, {
|
|
48
|
+
format,
|
|
49
|
+
serverUrl,
|
|
50
|
+
outputPath: opts.output,
|
|
51
|
+
});
|
|
52
|
+
const rel = path.relative(currentDir, outputPath);
|
|
53
|
+
console.log(chalk.green(` ✔ OpenAPI ${format.toUpperCase()} exported: ${rel}`));
|
|
54
|
+
console.log(chalk.gray(` Feature : ${dsl.feature.title}`));
|
|
55
|
+
console.log(chalk.gray(` Endpoints: ${dsl.endpoints.length}`));
|
|
56
|
+
console.log(chalk.gray(` Models : ${dsl.models.length}`));
|
|
57
|
+
console.log(chalk.gray(` Server : ${serverUrl}`));
|
|
58
|
+
console.log(chalk.blue("\n Next steps:"));
|
|
59
|
+
console.log(chalk.gray(` • Import ${rel} into Postman / Insomnia / Swagger UI`));
|
|
60
|
+
console.log(chalk.gray(` • Use openapi-generator to generate client SDKs`));
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error(chalk.red(` Export failed: ${(err as Error).message}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as fs from "fs-extra";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { createProvider, DEFAULT_MODELS, SUPPORTED_PROVIDERS } from "../../core/spec-generator";
|
|
6
|
+
import { ContextLoader } from "../../core/context-loader";
|
|
7
|
+
import { ConstitutionGenerator, CONSTITUTION_FILE } from "../../core/constitution-generator";
|
|
8
|
+
import { ConstitutionConsolidator } from "../../core/constitution-consolidator";
|
|
9
|
+
import {
|
|
10
|
+
loadGlobalConstitution,
|
|
11
|
+
saveGlobalConstitution,
|
|
12
|
+
GLOBAL_CONSTITUTION_FILE,
|
|
13
|
+
} from "../../core/global-constitution";
|
|
14
|
+
import {
|
|
15
|
+
globalConstitutionSystemPrompt,
|
|
16
|
+
buildGlobalConstitutionPrompt,
|
|
17
|
+
} from "../../prompts/global-constitution.prompt";
|
|
18
|
+
import { loadConfig, resolveApiKey } from "../utils";
|
|
19
|
+
|
|
20
|
+
export function registerInit(program: Command): void {
|
|
21
|
+
program
|
|
22
|
+
.command("init")
|
|
23
|
+
.description(`Analyze codebase and generate Project Constitution (${CONSTITUTION_FILE})`)
|
|
24
|
+
.option(
|
|
25
|
+
"--provider <name>",
|
|
26
|
+
`AI provider (${SUPPORTED_PROVIDERS.join("|")})`,
|
|
27
|
+
undefined
|
|
28
|
+
)
|
|
29
|
+
.option("--model <name>", "Model name")
|
|
30
|
+
.option("-k, --key <apiKey>", "API key")
|
|
31
|
+
.option("--force", "Overwrite existing constitution")
|
|
32
|
+
.option(
|
|
33
|
+
"--global",
|
|
34
|
+
`Generate a Global Constitution (~/${GLOBAL_CONSTITUTION_FILE}) instead of a project-level one`
|
|
35
|
+
)
|
|
36
|
+
.option("--consolidate", "Consolidate §9 accumulated lessons into §1–§8 core rules (prune & rebase)")
|
|
37
|
+
.option("--dry-run", "Preview consolidation result without writing (use with --consolidate)")
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
const currentDir = process.cwd();
|
|
40
|
+
const config = await loadConfig(currentDir);
|
|
41
|
+
|
|
42
|
+
const providerName = opts.provider || config.provider || "gemini";
|
|
43
|
+
const modelName = opts.model || config.model || DEFAULT_MODELS[providerName];
|
|
44
|
+
const apiKey = await resolveApiKey(providerName, opts.key);
|
|
45
|
+
const provider = createProvider(providerName, apiKey, modelName);
|
|
46
|
+
|
|
47
|
+
// ── Consolidate mode ───────────────────────────────────────────────────
|
|
48
|
+
if (opts.consolidate) {
|
|
49
|
+
const consolidator = new ConstitutionConsolidator(provider);
|
|
50
|
+
try {
|
|
51
|
+
const result = await consolidator.consolidate(currentDir, {
|
|
52
|
+
dryRun: opts.dryRun,
|
|
53
|
+
auto: opts.auto,
|
|
54
|
+
});
|
|
55
|
+
if (result.written) {
|
|
56
|
+
console.log(chalk.blue("\n Summary:"));
|
|
57
|
+
console.log(chalk.gray(` Lines : ${result.before.totalLines} → ${result.after.totalLines} (${result.before.totalLines - result.after.totalLines > 0 ? "-" : "+"}${Math.abs(result.before.totalLines - result.after.totalLines)})`));
|
|
58
|
+
console.log(chalk.gray(` §9 : ${result.before.lessonCount} → ${result.after.lessonCount} lessons remaining`));
|
|
59
|
+
if (result.backupPath) {
|
|
60
|
+
console.log(chalk.gray(` Backup: ${path.basename(result.backupPath)}`));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(chalk.red(` ✘ Consolidation failed: ${(err as Error).message}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Global constitution mode ───────────────────────────────────────────
|
|
71
|
+
if (opts.global) {
|
|
72
|
+
const existing = await loadGlobalConstitution([currentDir]);
|
|
73
|
+
if (existing && !opts.force) {
|
|
74
|
+
console.log(chalk.yellow(`\n Global constitution already exists at: ${existing.source}`));
|
|
75
|
+
console.log(chalk.gray(" Use --force to overwrite it."));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(chalk.blue("\n─── Generating Global Constitution ──────────────"));
|
|
80
|
+
console.log(chalk.gray(` Provider: ${providerName}/${modelName}`));
|
|
81
|
+
console.log(chalk.gray(" Scanning repos in workspace..."));
|
|
82
|
+
|
|
83
|
+
const loader = new ContextLoader(currentDir);
|
|
84
|
+
const ctx = await loader.loadProjectContext();
|
|
85
|
+
const summary = [
|
|
86
|
+
`Tech stack: ${ctx.techStack.join(", ") || "unknown"}`,
|
|
87
|
+
`Dependencies: ${ctx.dependencies.slice(0, 20).join(", ")}`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
|
|
90
|
+
const prompt = buildGlobalConstitutionPrompt([{ name: path.basename(currentDir), summary }]);
|
|
91
|
+
let globalConstitution: string;
|
|
92
|
+
try {
|
|
93
|
+
globalConstitution = await provider.generate(prompt, globalConstitutionSystemPrompt);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(chalk.red(" ✘ Failed to generate global constitution:"), err);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const saved = await saveGlobalConstitution(globalConstitution, currentDir);
|
|
100
|
+
console.log(chalk.green(`\n ✔ Global constitution saved: ${saved}`));
|
|
101
|
+
console.log(chalk.gray(" This will be automatically merged into all project constitutions in this workspace."));
|
|
102
|
+
console.log(chalk.gray(" Project-level rules always override global rules.\n"));
|
|
103
|
+
console.log(chalk.bold(" Preview:"));
|
|
104
|
+
console.log(chalk.gray(globalConstitution.split("\n").slice(0, 12).join("\n")));
|
|
105
|
+
if (globalConstitution.split("\n").length > 12) {
|
|
106
|
+
console.log(chalk.gray(` ... (${globalConstitution.split("\n").length} lines total)`));
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Project constitution mode (default) ───────────────────────────────
|
|
112
|
+
const constitutionPath = path.join(currentDir, CONSTITUTION_FILE);
|
|
113
|
+
|
|
114
|
+
if (!opts.force && (await fs.pathExists(constitutionPath))) {
|
|
115
|
+
console.log(chalk.yellow(`\n ${CONSTITUTION_FILE} already exists.`));
|
|
116
|
+
console.log(chalk.gray(" Use --force to overwrite it."));
|
|
117
|
+
console.log(chalk.gray(` Or edit it directly: ${constitutionPath}`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(chalk.blue("\n─── Generating Project Constitution ─────────────"));
|
|
122
|
+
console.log(chalk.gray(` Provider: ${providerName}/${modelName}`));
|
|
123
|
+
console.log(chalk.gray(" Analyzing codebase..."));
|
|
124
|
+
|
|
125
|
+
const generator = new ConstitutionGenerator(provider);
|
|
126
|
+
|
|
127
|
+
let constitution: string;
|
|
128
|
+
try {
|
|
129
|
+
constitution = await generator.generate(currentDir);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(chalk.red(" ✘ Failed to generate constitution:"), err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const saved = await generator.saveConstitution(currentDir, constitution);
|
|
136
|
+
|
|
137
|
+
const globalResult = await loadGlobalConstitution([path.dirname(currentDir)]);
|
|
138
|
+
if (globalResult) {
|
|
139
|
+
console.log(chalk.cyan(`\n ℹ Global constitution detected: ${globalResult.source}`));
|
|
140
|
+
console.log(chalk.gray(" It will be merged with this project constitution at runtime."));
|
|
141
|
+
console.log(chalk.gray(" Project rules take priority over global rules."));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(chalk.green(`\n ✔ Constitution saved: ${saved}`));
|
|
145
|
+
console.log(chalk.gray(" This file will be automatically used in all future `ai-spec create` runs."));
|
|
146
|
+
console.log(chalk.gray(" Edit it to add custom rules or red lines for your project.\n"));
|
|
147
|
+
console.log(chalk.bold(" Preview:"));
|
|
148
|
+
console.log(chalk.gray(constitution.split("\n").slice(0, 15).join("\n")));
|
|
149
|
+
if (constitution.split("\n").length > 15) {
|
|
150
|
+
console.log(chalk.gray(` ... (${constitution.split("\n").length} lines total)`));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { input } from "@inquirer/prompts";
|
|
4
|
+
import { appendDirectLesson } from "../../core/knowledge-memory";
|
|
5
|
+
|
|
6
|
+
export function registerLearn(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("learn")
|
|
9
|
+
.description("Append a lesson or engineering decision directly to constitution §9")
|
|
10
|
+
.argument("[lesson]", "The lesson or decision to record (prompted if omitted)")
|
|
11
|
+
.action(async (lesson: string | undefined) => {
|
|
12
|
+
const currentDir = process.cwd();
|
|
13
|
+
|
|
14
|
+
if (!lesson) {
|
|
15
|
+
lesson = await input({
|
|
16
|
+
message: "What lesson or engineering decision should be recorded?",
|
|
17
|
+
validate: (v) => v.trim().length > 0 || "Please enter a lesson",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = await appendDirectLesson(currentDir, lesson.trim());
|
|
22
|
+
|
|
23
|
+
if (result.appended) {
|
|
24
|
+
console.log(chalk.green(`\n ✔ Lesson appended to constitution §9`));
|
|
25
|
+
console.log(chalk.gray(` File: .ai-spec-constitution.md`));
|
|
26
|
+
} else {
|
|
27
|
+
console.log(chalk.yellow(`\n ⚠ Not appended: ${result.reason}`));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|