ai-spec-dev 0.25.0 → 0.29.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 +10 -0
- package/README.md +36 -7
- package/RELEASE_LOG.md +160 -0
- package/cli/index.ts +108 -4
- package/core/code-generator.ts +13 -1
- package/core/error-feedback.ts +18 -7
- package/core/frontend-context-loader.ts +72 -24
- package/core/provider-utils.ts +90 -0
- package/core/reviewer.ts +53 -11
- package/core/run-logger.ts +136 -0
- package/core/run-snapshot.ts +84 -0
- package/core/spec-generator.ts +83 -67
- package/core/task-generator.ts +11 -3
- package/dist/cli/index.js +1400 -997
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1399 -996
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +435 -232
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +435 -232
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +55 -19
- package/purpose.md +172 -33
package/README.md
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> AI 驱动的功能开发编排工具 — 从一句话需求到可运行代码的完整流水线,支持单 Repo 及多 Repo 跨端联动
|
|
4
4
|
|
|
5
|
-
[](https://github.com/hzhongzhong/ai-spec)
|
|
6
|
-
[](https://github.com/hzhongzhong)
|
|
7
|
-
|
|
8
5
|
**单 Repo 模式:**
|
|
9
6
|
```
|
|
10
|
-
需求描述 → 项目宪法 → 项目感知 → Spec+Tasks → 交互式润色(Diff预览) → Spec质量预评估 → Approval Gate → DSL提取+校验 → Git 隔离 → 代码生成(同层并行) → TDD测试(--tdd) / 测试骨架 → 错误反馈自动修复 →
|
|
7
|
+
需求描述 → 项目宪法 → 项目感知 → Spec+Tasks → 交互式润色(Diff预览) → Spec质量预评估 → Approval Gate → DSL提取+校验 → Git 隔离 → 代码生成(同层并行) → TDD测试(--tdd) / 测试骨架 → 错误反馈自动修复 → 3-pass 代码审查 → 经验积累(宪法§9)
|
|
11
8
|
```
|
|
12
9
|
|
|
13
10
|
**多 Repo 模式(工作区):**
|
|
@@ -94,9 +91,16 @@ ai-spec create "给用户模块增加登录功能"
|
|
|
94
91
|
[cycle 2/2] Running tests: npm test
|
|
95
92
|
✔ Tests passed.
|
|
96
93
|
✔ All checks passed after 2 cycle(s).
|
|
97
|
-
[9/9] Automated code review (
|
|
94
|
+
[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)...
|
|
95
|
+
Pass 1/3: Architecture review...
|
|
96
|
+
Pass 2/3: Implementation review...
|
|
97
|
+
Pass 3/3: Impact & complexity assessment...
|
|
98
|
+
🌊 影响等级: 中 🧮 复杂度等级: 低
|
|
98
99
|
─── Knowledge Memory ─────────────────────────────
|
|
99
100
|
✔ 2 lesson(s) appended to constitution (§9).
|
|
101
|
+
Run ID: 20260326-143022-ab3f in 94.3s · 8 files written
|
|
102
|
+
Log : .ai-spec-logs/20260326-143022-ab3f.json
|
|
103
|
+
To undo changes: ai-spec restore 20260326-143022-ab3f
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
---
|
|
@@ -153,7 +157,8 @@ ai-spec update [change] 增量更新:修改现有 Spec → 重提取 DSL
|
|
|
153
157
|
ai-spec learn [lesson] 零摩擦知识注入:直接将工程决策或教训写入宪法 §9(无需运行 review)
|
|
154
158
|
ai-spec export DSL → OpenAPI 3.1.0 YAML/JSON(可导入 Postman / Swagger UI / openapi-generator)
|
|
155
159
|
ai-spec mock 从 DSL 生成 Mock Server / 前端 Proxy 配置 / MSW Handlers(联调利器)
|
|
156
|
-
ai-spec review [file] 对当前 git diff 运行
|
|
160
|
+
ai-spec review [file] 对当前 git diff 运行 3-pass AI 代码审查(架构层 + 实现层 + 影响面/复杂度),并打印评分趋势
|
|
161
|
+
ai-spec restore <runId> 回滚指定 run 修改的所有文件至原始状态(配合 Run ID 使用)
|
|
157
162
|
ai-spec model 交互式切换 AI provider / model,写入 .ai-spec.json
|
|
158
163
|
ai-spec config 查看 / 修改 / 重置项目级配置
|
|
159
164
|
ai-spec workspace init 初始化多 Repo 工作区(生成 .ai-spec-workspace.json)
|
|
@@ -458,15 +463,34 @@ ai-spec review specs/feature-1234567890.md
|
|
|
458
463
|
ai-spec review --provider glm --model glm-5
|
|
459
464
|
```
|
|
460
465
|
|
|
461
|
-
|
|
466
|
+
**3-pass 输出结构:**
|
|
462
467
|
|
|
463
468
|
```
|
|
469
|
+
─── Pass 1/3: Architecture review ───────────────
|
|
464
470
|
## ✅ 优点 (What's Good)
|
|
465
471
|
## ⚠️ 问题 (Issues Found)
|
|
466
472
|
## 💡 改进建议 (Suggestions)
|
|
467
473
|
## 📊 总体评价 (Overall Assessment) Score: X/10
|
|
474
|
+
|
|
475
|
+
─── Pass 2/3: Implementation review ─────────────
|
|
476
|
+
## ✅ 优点 (What's Good)
|
|
477
|
+
## ⚠️ 问题 (Issues Found)
|
|
478
|
+
## 🔁 历史问题复现 (Recurring Issues)
|
|
479
|
+
## 💡 改进建议 (Suggestions)
|
|
480
|
+
## 📊 综合评分 (Final Score) Score: X/10
|
|
481
|
+
|
|
482
|
+
─── Pass 3/3: Impact & complexity assessment ────
|
|
483
|
+
## 🌊 影响面评估 (Impact Assessment)
|
|
484
|
+
直接影响文件 / 间接范围 / Breaking Changes / 影响等级: 低|中|高
|
|
485
|
+
## 🧮 代码复杂度评估 (Complexity Assessment)
|
|
486
|
+
认知复杂度热点 / 耦合度 / 可维护性风险 / 复杂度等级: 低|中|高
|
|
487
|
+
|
|
488
|
+
─── Review Score Trend ──────────────────────────
|
|
489
|
+
2026-03-26 [████████░░] 8/10 影响:中 复杂度:低 feature-login-v1.md
|
|
468
490
|
```
|
|
469
491
|
|
|
492
|
+
> Pass 3 的影响等级和复杂度等级会持久化到 `.ai-spec-reviews.json`,在历史趋势行中显示,三级颜色编码:高=红、中=黄、低=绿。
|
|
493
|
+
|
|
470
494
|
> 提示:先执行 `git add .` 再运行 `ai-spec review`,确保所有变更都被纳入审查。
|
|
471
495
|
|
|
472
496
|
---
|
|
@@ -595,6 +619,11 @@ ai-spec update "修复描述文字" --skip-affected
|
|
|
595
619
|
| `--provider <name>` | Spec 更新使用的 provider |
|
|
596
620
|
| `--codegen-provider <name>` | 代码生成使用的 provider |
|
|
597
621
|
|
|
622
|
+
> **`--codegen` 附带行为(v0.29.0+)**:
|
|
623
|
+
> - 每次 update 自动生成独立 Run ID,运行摘要(耗时、写入文件数)在结束时打印
|
|
624
|
+
> - 写每个受影响文件前先快照原始内容(`.ai-spec-backup/<runId>/`),可用 `ai-spec restore <runId>` 精确回滚
|
|
625
|
+
> - 完成后自动对更新后的 Spec 运行代码审查,结论写入宪法 §9(与 `create` 的知识积累机制一致)
|
|
626
|
+
|
|
598
627
|
---
|
|
599
628
|
|
|
600
629
|
### `ai-spec export`
|
package/RELEASE_LOG.md
CHANGED
|
@@ -2,6 +2,166 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## [0.29.0] 2026-03-27 — 全量审查修复(RunLogger 插桩、update 快照、Score Trend 升级、死代码清理)
|
|
6
|
+
|
|
7
|
+
### 修复内容
|
|
8
|
+
|
|
9
|
+
**Fix #1 — RunLogger 各阶段从未插桩(结构化日志实际为空)**
|
|
10
|
+
- 文件:`cli/index.ts`(create 命令主流水线)
|
|
11
|
+
- 问题:`core/run-logger.ts` 设计了完整的 `stageStart()`/`stageEnd()`/`stageFail()` API,但 CLI 中从未调用,生成的 `.ai-spec-logs/<runId>.json` 的 `entries[]` 数组始终为空——只有开始/结束时间戳和文件列表,丧失了每阶段耗时的核心价值。
|
|
12
|
+
- 修复:在 `create` 流水线所有主要阶段添加 stage 调用,覆盖以下 8 个节点:
|
|
13
|
+
|
|
14
|
+
| Stage Key | 对应步骤 | 记录数据 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `context_load` | Step 1 — 加载项目上下文 | `techStack`, `repoType` |
|
|
17
|
+
| `spec_gen` | Step 2 — Spec + Tasks 生成 | `provider`, `model`, `taskCount`;失败时 `stageFail` |
|
|
18
|
+
| `spec_refine` | Step 3 — 交互式润色 | 耗时 |
|
|
19
|
+
| `spec_assess` | Step 3.4 — Spec 质量评估 | `overallScore`;gate 失败时 `stageFail` |
|
|
20
|
+
| `dsl_extract` | DSL — 结构化提取 | `endpoints`, `models` 数量;提取失败时 `stageFail` |
|
|
21
|
+
| `codegen` | Step 6 — 代码生成 | `mode`, `provider`, `model`, `filesGenerated` |
|
|
22
|
+
| `test_gen` | Step 7 — 测试骨架生成 | `filesGenerated` |
|
|
23
|
+
| `error_feedback` | Step 8 — 错误反馈闭环 | 耗时 |
|
|
24
|
+
| `review` | Step 9 — 3-pass 代码审查 | 耗时 |
|
|
25
|
+
|
|
26
|
+
**Fix #2 — `update --codegen` 写文件前无快照,运行结束无日志**
|
|
27
|
+
- 文件:`cli/index.ts`(update 命令)
|
|
28
|
+
- 问题:`update --codegen` 直接改写受影响文件,但 `setActiveSnapshot()`/`setActiveLogger()` 只在 `create` 命令中调用,`getActiveSnapshot()?.snapshotFile()` 因返回 `null` 而静默跳过,用户无法对 update 产生的改动执行 `ai-spec restore`。
|
|
29
|
+
- 修复:
|
|
30
|
+
- update 命令启动时生成独立 `updateRunId`,初始化 `RunSnapshot` 和 `RunLogger` 并注册为 active 单例
|
|
31
|
+
- 写每个受影响文件前调用 `updateSnapshot.snapshotFile(fullPath)`,现在 `ai-spec restore <updateRunId>` 对 update 生成的改动同样有效
|
|
32
|
+
- 写文件后调用 `updateLogger.fileWritten()`
|
|
33
|
+
- `update_codegen` stage 包含 `stageStart`/`stageEnd`/`stageFail`
|
|
34
|
+
- 命令结束时 `finish()` + `printSummary()` + restore 提示(与 create 对齐)
|
|
35
|
+
|
|
36
|
+
**Fix #3 — `update --codegen` 结束后不积累审查知识**
|
|
37
|
+
- 文件:`cli/index.ts`(update 命令)
|
|
38
|
+
- 问题:`create` 流水线最后会用 `accumulateReviewKnowledge()` 把 review 结论写入宪法 §9;`update --codegen` 虽然也修改了代码,但流程结束后什么都不写。团队在 update 阶段发现的问题从来不进入知识库,宪法无法从迭代修改中学习。
|
|
39
|
+
- 修复:`update --codegen` 完成文件写入后,自动对更新后的 Spec 运行一次 `reviewer.reviewCode()`,并将结果传入 `accumulateReviewKnowledge()`,复用 spec 更新阶段已创建的 `provider` 实例,无需额外配置。
|
|
40
|
+
|
|
41
|
+
**Fix #4 — `reviewSystemPrompt` 旧版单体 prompt 变为死代码未清理**
|
|
42
|
+
- 文件:`prompts/codegen.prompt.ts`、`core/reviewer.ts`
|
|
43
|
+
- 问题:v0.28.0 升级为 3-pass 后,旧的整合式 `reviewSystemPrompt`(18 行)仍在 `codegen.prompt.ts` 中导出,并被 `reviewer.ts` import 但不在任何地方调用,造成维护混淆。
|
|
44
|
+
- 修复:删除 `codegen.prompt.ts` 中的 `reviewSystemPrompt` export,同步删除 `reviewer.ts` 中的对应 import;注释由 `// ─── Two-pass review prompts` 更新为 `// ─── 3-pass review prompts`。
|
|
45
|
+
|
|
46
|
+
### 功能增强
|
|
47
|
+
|
|
48
|
+
**Enhancement — `printScoreTrend()` 新增影响等级 / 复杂度等级展示**
|
|
49
|
+
- 文件:`core/reviewer.ts`
|
|
50
|
+
- 背景:v0.28.0 审查历史 `ReviewHistoryEntry` 已增加 `impactLevel` 和 `complexityLevel` 字段并持久化,但 `printScoreTrend()` 展示趋势时从未读取这两个字段,用户看不到任何影响 / 复杂度信息。
|
|
51
|
+
- 修复:每行趋势输出追加两个彩色标签,颜色编码:高=红、中=黄、低=绿。
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
前(只有分数):
|
|
55
|
+
2026-03-26 [████████░░] 8/10 feature-tasks-v1.md
|
|
56
|
+
|
|
57
|
+
后(新增等级标签):
|
|
58
|
+
2026-03-26 [████████░░] 8/10 影响:中 复杂度:低 feature-tasks-v1.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## [0.28.0] 2026-03-26 — 三 Pass 代码审查(影响面评估 + 代码复杂度评估)
|
|
64
|
+
|
|
65
|
+
### 新增内容
|
|
66
|
+
|
|
67
|
+
**Feature — Review Pass 3:影响面评估 + 代码复杂度评估**
|
|
68
|
+
- 文件:`prompts/codegen.prompt.ts`(新增 `reviewImpactComplexitySystemPrompt`)、`core/reviewer.ts`(两 Pass 升级为三 Pass)
|
|
69
|
+
- 原有两 Pass 不变;新增第三 Pass 专注于两个前两 Pass 刻意跳过的维度:
|
|
70
|
+
|
|
71
|
+
**影响面评估 (Impact Assessment)**
|
|
72
|
+
- 直接影响文件列表
|
|
73
|
+
- 间接影响范围(哪些模块/消费方/下游服务受影响)
|
|
74
|
+
- 破坏性变更检测(接口签名变更、Schema 变更、配置变更、导出重命名)
|
|
75
|
+
- 影响等级:低 / 中 / 高(附理由)
|
|
76
|
+
|
|
77
|
+
**代码复杂度评估 (Complexity Assessment)**
|
|
78
|
+
- 认知复杂度热点(最难理解的 1-3 个函数,说明为什么复杂)
|
|
79
|
+
- 耦合度分析(依赖注入 vs 硬编码、循环依赖风险)
|
|
80
|
+
- 可维护性风险(魔法数字、业务逻辑藏在生命周期钩子里、隐式时序耦合)
|
|
81
|
+
- 复杂度等级:低 / 中 / 高(附理由)
|
|
82
|
+
|
|
83
|
+
- `ReviewHistoryEntry` 新增 `impactLevel` 和 `complexityLevel` 字段,历史记录持久化到 `.ai-spec-reviews.json`
|
|
84
|
+
- CLI banner 更新为 `3-pass: architecture + implementation + impact/complexity`
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## [0.27.0] 2026-03-26 — 三项工业化底座(Provider 可靠性、文件快照回滚、RunId 结构化日志)
|
|
89
|
+
|
|
90
|
+
### 新增内容
|
|
91
|
+
|
|
92
|
+
**Feature #1 — Provider 统一可靠性封装**
|
|
93
|
+
- 新文件:`core/provider-utils.ts`
|
|
94
|
+
- 新增 `withReliability(fn, opts)` 包装器,覆盖所有 provider 的 `generate()` 调用(Gemini、Claude、OpenAI-compatible、MiMo)
|
|
95
|
+
- 能力:超时(默认 90s)+ 自动重试(2 次,退避 2s/6s)+ 结构化错误分类(`auth` / `rate_limit` / `timeout` / `network` / `provider`)
|
|
96
|
+
- Auth 错误(401/403)不重试,避免无效消耗;限流(429)和网络抖动均自动重试并打印黄色警告
|
|
97
|
+
|
|
98
|
+
**Feature #2 — 文件写入快照与一键回滚**
|
|
99
|
+
- 新文件:`core/run-snapshot.ts`
|
|
100
|
+
- 每次 `create` 运行前,自动备份将被覆盖的文件到 `.ai-spec-backup/<runId>/`
|
|
101
|
+
- 新增命令:`ai-spec restore <runId>`,将本次运行修改的所有文件恢复到原始状态
|
|
102
|
+
- 涵盖 codegen 写入(`code-generator.ts`)和错误修复写入(`error-feedback.ts`)两个落盘点
|
|
103
|
+
- 纯新建文件不备份(无需恢复);同一文件多次写入只备份一次(保留原始版本)
|
|
104
|
+
|
|
105
|
+
**Feature #3 — RunId + 结构化执行日志**
|
|
106
|
+
- 新文件:`core/run-logger.ts`
|
|
107
|
+
- 每次运行生成唯一 RunId(格式:`YYYYMMDD-HHMMSS-xxxx`),打印在 banner 下方
|
|
108
|
+
- 执行阶段、写入文件、错误信息实时写入 `.ai-spec-logs/<runId>.json`
|
|
109
|
+
- 运行结束时打印摘要:RunId + 耗时 + 写入文件数 + 错误数 + 日志路径
|
|
110
|
+
- 有文件被修改时自动提示:`To undo changes: ai-spec restore <runId>`
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## [0.26.0] 2026-03-26 — 三项稳定性修复(多仓库 review、并行 batch 容错、tasks JSON 损坏)
|
|
115
|
+
|
|
116
|
+
### 修复内容
|
|
117
|
+
|
|
118
|
+
**Fix #1 — 多仓库模式代码审查 git diff 为空**
|
|
119
|
+
- 文件:`cli/index.ts` → `runSingleRepoPipelineInWorkspace`
|
|
120
|
+
- 问题:`reviewer.reviewCode()` 内部调用 `execSync("git diff")`,运行在 `process.cwd()`(CLI 启动目录)而非当前 repo 的 `workingDir`(可能是 worktree 路径),导致 diff 为空或错误,审查结果没有意义。
|
|
121
|
+
- 修复:在 `reviewCode` 调用前后加 `process.chdir(workingDir)` / `process.chdir(originalDir)`,与单仓库模式保持一致。
|
|
122
|
+
|
|
123
|
+
**Fix #2 — 并行 batch 单任务抛异常导致整层崩溃**
|
|
124
|
+
- 文件:`core/code-generator.ts` → `runApiModeWithTasks` batch 执行循环
|
|
125
|
+
- 问题:`Promise.all(batchResultPromises)` 中任意一个 `executeTask` 抛出未捕获异常(磁盘满、mkdir 失败、provider 超时),整个 `Promise.all` 立即 reject,该层剩余所有任务都被丢弃,没有任何降级处理。
|
|
126
|
+
- 修复:每个 `executeTask` 调用后追加 `.catch((err) => ...)` 返回失败结果对象,确保单任务失败只影响自身,不中断同批次其他任务。
|
|
127
|
+
|
|
128
|
+
**Fix #3 — `loadTasksForSpec` 遇到损坏的 JSON 文件直接崩溃**
|
|
129
|
+
- 文件:`core/task-generator.ts` → `loadTasksForSpec`
|
|
130
|
+
- 问题:如果上次运行中途中断导致 `*-tasks.json` 是不完整的 JSON,`fs.readJson()` 抛出 parse 错误,没有任何 try-catch 包裹,用户看到的是裸 JS 异常而非友好提示。
|
|
131
|
+
- 修复:加 try-catch,catch 块打印"Tasks file corrupt,请重新运行 `ai-spec tasks`"并返回 `null`(触发重新生成),不再崩溃。
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## [0.25.0] 2026-03-26 — 三项上下文提取修复(HTTP import、分页示例、工具崩溃误判)
|
|
136
|
+
|
|
137
|
+
### 修复内容
|
|
138
|
+
|
|
139
|
+
**Fix #1 — HTTP import 幻觉防护失效**
|
|
140
|
+
- 文件:`core/frontend-context-loader.ts` → `httpImportRegex`
|
|
141
|
+
- 问题:旧正则只匹配 `axios`、`ky`、`@/` 开头的路径。使用 `import request from '@/utils/request'` 等自定义封装的项目(极其常见)提取结果为 `undefined`,AI 会自由发挥 import 路径。
|
|
142
|
+
- 修复:扩展匹配范围:
|
|
143
|
+
- 所有项目别名:`@/`、`~/`、`#/`、`@@/`
|
|
144
|
+
- 包含 http/request/fetch/client/api 关键词的相对路径
|
|
145
|
+
- 完整 HTTP 库列表:axios、ky、ky-universal、undici、node-fetch、cross-fetch、got、superagent、alova、openapi-fetch
|
|
146
|
+
- 排除 `import type` 语句(它们不是运行时 import)
|
|
147
|
+
|
|
148
|
+
**Fix #2 — 分页示例提取正则永远不匹配**
|
|
149
|
+
- 文件:`core/frontend-context-loader.ts` → 分页提取块
|
|
150
|
+
- 问题:
|
|
151
|
+
1. 接口正则用 `[^}]*` 匹配接口体,遇到嵌套对象 `{ field: { ... } }` 立即截断
|
|
152
|
+
2. 函数正则用 `\n\}` 匹配闭合括号,但缩进的 ` }` 永远不匹配
|
|
153
|
+
3. 只处理 `export function`,遗漏了现代代码中更常见的 `export const x = () =>` 写法
|
|
154
|
+
- 修复:完全重写为**逐行 + 括号深度计数器**的两步提取法:
|
|
155
|
+
- Step 1:找到带分页字段(pageIndex/pageSize/page/…)的接口,用深度计数捕获完整块(支持嵌套对象)
|
|
156
|
+
- Step 2:找到引用该接口的导出函数(同时支持 `export function` 和 `export const = () =>`),同样用深度计数捕获函数体
|
|
157
|
+
|
|
158
|
+
**Fix #3 — `isToolCrash` 把用户代码错误当工具崩溃**
|
|
159
|
+
- 文件:`core/error-feedback.ts`
|
|
160
|
+
- 问题:旧判断条件是"输出包含 ReferenceError/TypeError 且包含 node_modules"。TypeScript 的自动修复测试运行时,测试框架的 stack trace 中也会包含 node_modules,导致用户自己代码里的 ReferenceError 被误判为工具崩溃跳过。
|
|
161
|
+
- 修复:改为精确判断:必须同时满足(1)存在未捕获 JS 错误,且(2)stack trace 中有 `at … node_modules/…` 帧——即错误起源于工具二进制本身,而不仅仅是"输出中某处出现了 node_modules"。
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
5
165
|
## [0.24.0] 2026-03-25 — 四项质量修复(lesson 计数、export default、impliesRegistration、依赖拓扑排序)
|
|
6
166
|
|
|
7
167
|
### 修复内容
|
package/cli/index.ts
CHANGED
|
@@ -72,6 +72,8 @@ import {
|
|
|
72
72
|
} from "../core/mock-server-generator";
|
|
73
73
|
import { SpecUpdater } from "../core/spec-updater";
|
|
74
74
|
import { exportOpenApi } from "../core/openapi-exporter";
|
|
75
|
+
import { generateRunId, RunLogger, setActiveLogger } from "../core/run-logger";
|
|
76
|
+
import { RunSnapshot, setActiveSnapshot } from "../core/run-snapshot";
|
|
75
77
|
|
|
76
78
|
// ─── Config File ──────────────────────────────────────────────────────────────
|
|
77
79
|
|
|
@@ -293,11 +295,24 @@ program
|
|
|
293
295
|
codegenModel: codegenModelName,
|
|
294
296
|
});
|
|
295
297
|
|
|
298
|
+
// ── Run tracking ──────────────────────────────────────────────────────────────
|
|
299
|
+
const runId = generateRunId();
|
|
300
|
+
console.log(chalk.gray(` Run ID: ${runId}`));
|
|
301
|
+
const runSnapshot = new RunSnapshot(currentDir, runId);
|
|
302
|
+
setActiveSnapshot(runSnapshot);
|
|
303
|
+
const runLogger = new RunLogger(currentDir, runId, {
|
|
304
|
+
provider: specProviderName,
|
|
305
|
+
model: specModelName,
|
|
306
|
+
});
|
|
307
|
+
setActiveLogger(runLogger);
|
|
308
|
+
|
|
296
309
|
// ── Step 1: Context ───────────────────────────────────────────────────────
|
|
297
310
|
console.log(chalk.blue("[1/6] Loading project context..."));
|
|
311
|
+
runLogger.stageStart("context_load");
|
|
298
312
|
const loader = new ContextLoader(currentDir);
|
|
299
313
|
const context = await loader.loadProjectContext();
|
|
300
314
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
315
|
+
runLogger.stageEnd("context_load", { techStack: context.techStack, repoType: detectedRepoType });
|
|
301
316
|
console.log(chalk.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
302
317
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
303
318
|
console.log(chalk.gray(` API files : ${context.apiStructure.length} files`));
|
|
@@ -332,6 +347,7 @@ program
|
|
|
332
347
|
let initialSpec: string;
|
|
333
348
|
let initialTasks: import("../core/task-generator").SpecTask[] = [];
|
|
334
349
|
|
|
350
|
+
runLogger.stageStart("spec_gen", { provider: specProviderName, model: specModelName });
|
|
335
351
|
try {
|
|
336
352
|
if (opts.skipTasks) {
|
|
337
353
|
// Tasks skipped: use SpecGenerator alone
|
|
@@ -350,7 +366,9 @@ program
|
|
|
350
366
|
console.log(chalk.yellow(" ⚠ Tasks not parsed from response — will retry separately after refinement."));
|
|
351
367
|
}
|
|
352
368
|
}
|
|
369
|
+
runLogger.stageEnd("spec_gen", { taskCount: initialTasks.length });
|
|
353
370
|
} catch (err) {
|
|
371
|
+
runLogger.stageFail("spec_gen", (err as Error).message);
|
|
354
372
|
console.error(chalk.red(" ✘ Spec generation failed:"), err);
|
|
355
373
|
process.exit(1);
|
|
356
374
|
}
|
|
@@ -362,8 +380,10 @@ program
|
|
|
362
380
|
finalSpec = initialSpec;
|
|
363
381
|
} else {
|
|
364
382
|
console.log(chalk.blue("\n[3/6] Interactive spec refinement..."));
|
|
383
|
+
runLogger.stageStart("spec_refine");
|
|
365
384
|
const refiner = new SpecRefiner(specProvider);
|
|
366
385
|
finalSpec = await refiner.refineLoop(initialSpec);
|
|
386
|
+
runLogger.stageEnd("spec_refine");
|
|
367
387
|
}
|
|
368
388
|
|
|
369
389
|
// Compute slug once — used in Approval Gate diff preview and versioned file save
|
|
@@ -380,14 +400,17 @@ program
|
|
|
380
400
|
if (!opts.auto) {
|
|
381
401
|
console.log(chalk.blue("\n[3.4/6] Spec quality assessment..."));
|
|
382
402
|
}
|
|
403
|
+
runLogger.stageStart("spec_assess");
|
|
383
404
|
const assessment = await assessSpec(specProvider, finalSpec, context.constitution ?? undefined);
|
|
384
405
|
if (assessment) {
|
|
406
|
+
runLogger.stageEnd("spec_assess", { overallScore: assessment.overallScore });
|
|
385
407
|
if (!opts.auto) printSpecAssessment(assessment);
|
|
386
408
|
|
|
387
409
|
if (minScore > 0 && assessment.overallScore < minScore) {
|
|
388
410
|
if (opts.force) {
|
|
389
411
|
console.log(chalk.yellow(`\n ⚠ Score gate: ${assessment.overallScore}/10 < minimum ${minScore}/10 — bypassed with --force.`));
|
|
390
412
|
} else {
|
|
413
|
+
runLogger.stageFail("spec_assess", `Score gate: ${assessment.overallScore} < ${minScore}`);
|
|
391
414
|
console.log(chalk.red(`\n ✘ Spec quality gate failed: overallScore ${assessment.overallScore}/10 < minimum ${minScore}/10`));
|
|
392
415
|
if (!opts.auto) {
|
|
393
416
|
console.log(chalk.gray(` Address the issues above and re-run, or use --force to bypass.`));
|
|
@@ -398,8 +421,11 @@ program
|
|
|
398
421
|
process.exit(1);
|
|
399
422
|
}
|
|
400
423
|
}
|
|
401
|
-
} else
|
|
402
|
-
|
|
424
|
+
} else {
|
|
425
|
+
runLogger.stageEnd("spec_assess", { skipped: true });
|
|
426
|
+
if (!opts.auto) {
|
|
427
|
+
console.log(chalk.gray(" (Assessment skipped — AI call failed or timed out)"));
|
|
428
|
+
}
|
|
403
429
|
}
|
|
404
430
|
}
|
|
405
431
|
|
|
@@ -472,18 +498,22 @@ program
|
|
|
472
498
|
} else {
|
|
473
499
|
console.log(chalk.blue("\n[DSL] Extracting structured DSL from spec..."));
|
|
474
500
|
console.log(chalk.gray(` Provider: ${specProviderName}/${specModelName}`));
|
|
501
|
+
runLogger.stageStart("dsl_extract");
|
|
475
502
|
try {
|
|
476
503
|
const isFrontend = isFrontendDeps(context.dependencies);
|
|
477
504
|
if (isFrontend) console.log(chalk.gray(" Frontend project detected — using ComponentSpec extractor"));
|
|
478
505
|
const dslExtractor = new DslExtractor(specProvider);
|
|
479
506
|
extractedDsl = await dslExtractor.extract(finalSpec, { auto: opts.auto, isFrontend });
|
|
480
507
|
if (extractedDsl) {
|
|
508
|
+
runLogger.stageEnd("dsl_extract", { endpoints: extractedDsl.endpoints?.length ?? 0, models: extractedDsl.models?.length ?? 0 });
|
|
481
509
|
console.log(chalk.green(" ✔ DSL extracted and validated."));
|
|
482
510
|
} else {
|
|
511
|
+
runLogger.stageEnd("dsl_extract", { skipped: true });
|
|
483
512
|
console.log(chalk.yellow(" ⚠ DSL skipped — codegen will use Spec + Tasks only."));
|
|
484
513
|
}
|
|
485
514
|
} catch (err) {
|
|
486
515
|
// Unexpected error (not user abort — that would have called process.exit)
|
|
516
|
+
runLogger.stageFail("dsl_extract", (err as Error).message);
|
|
487
517
|
console.log(chalk.yellow(` ⚠ DSL extraction error: ${(err as Error).message} — continuing without DSL.`));
|
|
488
518
|
}
|
|
489
519
|
}
|
|
@@ -568,6 +598,7 @@ program
|
|
|
568
598
|
generatedTestFiles = await testGen.generateTdd(extractedDsl, workingDir);
|
|
569
599
|
}
|
|
570
600
|
|
|
601
|
+
runLogger.stageStart("codegen", { mode: codegenMode, provider: codegenProviderName, model: codegenModelName });
|
|
571
602
|
const codegen = new CodeGenerator(codegenProvider, codegenMode);
|
|
572
603
|
const generatedFiles = await codegen.generateCode(specFile, workingDir, context, {
|
|
573
604
|
auto: opts.auto,
|
|
@@ -575,6 +606,7 @@ program
|
|
|
575
606
|
dslFilePath: savedDslFile ?? undefined,
|
|
576
607
|
repoType: detectedRepoType,
|
|
577
608
|
});
|
|
609
|
+
runLogger.stageEnd("codegen", { filesGenerated: generatedFiles.length });
|
|
578
610
|
|
|
579
611
|
// ── Step 7: Test Skeleton Generation (skipped in TDD mode — tests already written) ──
|
|
580
612
|
if (opts.tdd) {
|
|
@@ -585,8 +617,10 @@ program
|
|
|
585
617
|
console.log(chalk.gray("\n[7/9] Skipping test generation (no DSL available)."));
|
|
586
618
|
} else {
|
|
587
619
|
console.log(chalk.blue(`\n[7/9] Test skeleton generation...`));
|
|
620
|
+
runLogger.stageStart("test_gen");
|
|
588
621
|
const testGen = new TestGenerator(codegenProvider);
|
|
589
622
|
generatedTestFiles = await testGen.generate(extractedDsl, workingDir);
|
|
623
|
+
runLogger.stageEnd("test_gen", { filesGenerated: generatedTestFiles.length });
|
|
590
624
|
}
|
|
591
625
|
|
|
592
626
|
// ── Step 8: Error Feedback Loop ───────────────────────────────────────────
|
|
@@ -598,15 +632,18 @@ program
|
|
|
598
632
|
if (opts.tdd) {
|
|
599
633
|
console.log(chalk.cyan("[8/9] TDD mode — error feedback loop driving implementation to pass tests..."));
|
|
600
634
|
}
|
|
635
|
+
runLogger.stageStart("error_feedback");
|
|
601
636
|
await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
602
637
|
maxCycles: opts.tdd ? 3 : 2, // TDD gets one extra cycle
|
|
603
638
|
});
|
|
639
|
+
runLogger.stageEnd("error_feedback");
|
|
604
640
|
}
|
|
605
641
|
|
|
606
642
|
// ── Step 9: Code Review ───────────────────────────────────────────────────
|
|
607
643
|
let reviewResult = "";
|
|
608
644
|
if (!opts.skipReview) {
|
|
609
|
-
console.log(chalk.blue("\n[9/9] Automated code review (
|
|
645
|
+
console.log(chalk.blue("\n[9/9] Automated code review (3-pass: architecture + implementation + impact/complexity)..."));
|
|
646
|
+
runLogger.stageStart("review");
|
|
610
647
|
const reviewer = new CodeReviewer(specProvider, currentDir);
|
|
611
648
|
const savedSpec = await fs.readFile(specFile, "utf-8");
|
|
612
649
|
|
|
@@ -623,12 +660,14 @@ program
|
|
|
623
660
|
process.chdir(originalDir);
|
|
624
661
|
}
|
|
625
662
|
}
|
|
663
|
+
runLogger.stageEnd("review");
|
|
626
664
|
|
|
627
665
|
// Knowledge Memory: extract lessons from review and append to constitution §9
|
|
628
666
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
629
667
|
}
|
|
630
668
|
|
|
631
669
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
670
|
+
runLogger.finish();
|
|
632
671
|
console.log(chalk.bold.green("\n✔ All done!"));
|
|
633
672
|
console.log(chalk.gray(` Spec : ${specFile}`));
|
|
634
673
|
if (savedDslFile) console.log(chalk.gray(` DSL : ${savedDslFile}`));
|
|
@@ -639,6 +678,10 @@ program
|
|
|
639
678
|
if (workingDir !== currentDir) {
|
|
640
679
|
console.log(chalk.gray(` Run \`cd ${workingDir}\` to enter the worktree.`));
|
|
641
680
|
}
|
|
681
|
+
runLogger.printSummary();
|
|
682
|
+
if (runSnapshot.fileCount > 0) {
|
|
683
|
+
console.log(chalk.gray(` To undo changes: ai-spec restore ${runId}`));
|
|
684
|
+
}
|
|
642
685
|
});
|
|
643
686
|
|
|
644
687
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1252,7 +1295,16 @@ async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
1252
1295
|
console.log(chalk.blue(` [${repoName}] Running code review...`));
|
|
1253
1296
|
try {
|
|
1254
1297
|
const reviewer = new CodeReviewer(specProvider);
|
|
1255
|
-
|
|
1298
|
+
// git diff must run in the repo's working directory (may be a worktree),
|
|
1299
|
+
// not in wherever the CLI was invoked from.
|
|
1300
|
+
const originalDir = process.cwd();
|
|
1301
|
+
let reviewResult: string;
|
|
1302
|
+
try {
|
|
1303
|
+
process.chdir(workingDir);
|
|
1304
|
+
reviewResult = await reviewer.reviewCode(finalSpec);
|
|
1305
|
+
} finally {
|
|
1306
|
+
process.chdir(originalDir);
|
|
1307
|
+
}
|
|
1256
1308
|
await accumulateReviewKnowledge(specProvider, repoAbsPath, reviewResult);
|
|
1257
1309
|
console.log(chalk.green(` Code review complete.`));
|
|
1258
1310
|
} catch (err) {
|
|
@@ -1763,6 +1815,14 @@ program
|
|
|
1763
1815
|
console.log(chalk.blue("\n─── ai-spec update ─────────────────────────────"));
|
|
1764
1816
|
console.log(chalk.gray(` Provider: ${providerName}/${modelName}`));
|
|
1765
1817
|
|
|
1818
|
+
// ── Run tracking (snapshot + log) ─────────────────────────────────────────
|
|
1819
|
+
const updateRunId = generateRunId();
|
|
1820
|
+
const updateSnapshot = new RunSnapshot(currentDir, updateRunId);
|
|
1821
|
+
setActiveSnapshot(updateSnapshot);
|
|
1822
|
+
const updateLogger = new RunLogger(currentDir, updateRunId, { provider: providerName, model: modelName });
|
|
1823
|
+
setActiveLogger(updateLogger);
|
|
1824
|
+
console.log(chalk.gray(` Run ID: ${updateRunId}`));
|
|
1825
|
+
|
|
1766
1826
|
// ── Find existing spec ────────────────────────────────────────────────────
|
|
1767
1827
|
let specPath: string | null = opts.spec ?? null;
|
|
1768
1828
|
if (!specPath) {
|
|
@@ -1833,6 +1893,7 @@ program
|
|
|
1833
1893
|
? `\n=== DSL Context ===\n${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3000)}\n`
|
|
1834
1894
|
: "";
|
|
1835
1895
|
|
|
1896
|
+
updateLogger.stageStart("update_codegen");
|
|
1836
1897
|
for (const affected of result.affectedFiles) {
|
|
1837
1898
|
const fullPath = path.join(currentDir, affected.file);
|
|
1838
1899
|
let existing = "";
|
|
@@ -1856,12 +1917,34 @@ ${existing || "Create from scratch."}`;
|
|
|
1856
1917
|
const raw = await codegenProvider.generate(codePrompt, _getPrompt(repoType));
|
|
1857
1918
|
const content = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
1858
1919
|
await fs.ensureDir(path.dirname(fullPath));
|
|
1920
|
+
// Snapshot original before overwrite so `ai-spec restore` can undo
|
|
1921
|
+
await updateSnapshot.snapshotFile(fullPath);
|
|
1859
1922
|
await fs.writeFile(fullPath, content, "utf-8");
|
|
1923
|
+
updateLogger.fileWritten(affected.file);
|
|
1860
1924
|
console.log(chalk.green("✔"));
|
|
1861
1925
|
} catch (err) {
|
|
1926
|
+
updateLogger.stageFail("update_codegen", `${affected.file}: ${(err as Error).message}`);
|
|
1862
1927
|
console.log(chalk.red(`✘ ${(err as Error).message}`));
|
|
1863
1928
|
}
|
|
1864
1929
|
}
|
|
1930
|
+
updateLogger.stageEnd("update_codegen", { filesUpdated: result.affectedFiles.length });
|
|
1931
|
+
|
|
1932
|
+
// Knowledge Memory: run a lightweight review on the updated spec and accumulate lessons
|
|
1933
|
+
const updatedSpecContent = await fs.readFile(result.newSpecPath, "utf-8").catch(() => "");
|
|
1934
|
+
if (updatedSpecContent) {
|
|
1935
|
+
const updateReviewer = new CodeReviewer(provider, currentDir);
|
|
1936
|
+
const reviewResult = await updateReviewer.reviewCode(updatedSpecContent, result.newSpecPath).catch(() => "");
|
|
1937
|
+
if (reviewResult && reviewResult !== "No changes") {
|
|
1938
|
+
await accumulateReviewKnowledge(provider, currentDir, reviewResult);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// ── Finish run tracking ────────────────────────────────────────────────────
|
|
1944
|
+
updateLogger.finish();
|
|
1945
|
+
updateLogger.printSummary();
|
|
1946
|
+
if (updateSnapshot.fileCount > 0) {
|
|
1947
|
+
console.log(chalk.gray(` To undo changes: ai-spec restore ${updateRunId}`));
|
|
1865
1948
|
}
|
|
1866
1949
|
|
|
1867
1950
|
// ── Hints ─────────────────────────────────────────────────────────────────
|
|
@@ -2127,6 +2210,27 @@ program
|
|
|
2127
2210
|
}
|
|
2128
2211
|
});
|
|
2129
2212
|
|
|
2213
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2214
|
+
// Command: restore
|
|
2215
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2216
|
+
|
|
2217
|
+
program
|
|
2218
|
+
.command("restore")
|
|
2219
|
+
.description("Restore files modified by a previous run")
|
|
2220
|
+
.argument("<runId>", "Run ID shown at the end of a create / generate run")
|
|
2221
|
+
.action(async (runId: string) => {
|
|
2222
|
+
const currentDir = process.cwd();
|
|
2223
|
+
const snapshot = new RunSnapshot(currentDir, runId);
|
|
2224
|
+
console.log(chalk.blue(`Restoring run: ${runId}...`));
|
|
2225
|
+
const restored = await snapshot.restore();
|
|
2226
|
+
if (restored.length === 0) {
|
|
2227
|
+
console.log(chalk.yellow(" No backup found for this run ID."));
|
|
2228
|
+
} else {
|
|
2229
|
+
restored.forEach((f) => console.log(chalk.green(` ✔ restored: ${f}`)));
|
|
2230
|
+
console.log(chalk.bold.green(`\n✔ ${restored.length} file(s) restored.`));
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2130
2234
|
// Show welcome screen when invoked with no arguments
|
|
2131
2235
|
if (process.argv.length <= 2) {
|
|
2132
2236
|
(async () => {
|
package/core/code-generator.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { getCodeGenSystemPrompt } from "../prompts/codegen.prompt";
|
|
|
8
8
|
import { SpecTask, loadTasksForSpec, updateTaskStatus } from "./task-generator";
|
|
9
9
|
import { loadDslForSpec, buildDslContextSection } from "./dsl-extractor";
|
|
10
10
|
import { loadFrontendContext, buildFrontendContextSection } from "./frontend-context-loader";
|
|
11
|
+
import { getActiveSnapshot } from "./run-snapshot";
|
|
12
|
+
import { getActiveLogger } from "./run-logger";
|
|
11
13
|
|
|
12
14
|
// ─── Shared Config Helper ───────────────────────────────────────────────────
|
|
13
15
|
|
|
@@ -720,7 +722,15 @@ Output ONLY a valid JSON array:
|
|
|
720
722
|
|
|
721
723
|
for (const batch of taskBatches) {
|
|
722
724
|
const batchIsParallel = batch.length > 1;
|
|
723
|
-
|
|
725
|
+
// Wrap each task in .catch() so a single unexpected failure (disk full,
|
|
726
|
+
// provider timeout, mkdir error) degrades gracefully instead of rejecting
|
|
727
|
+
// the entire Promise.all and aborting all sibling tasks in the batch.
|
|
728
|
+
const batchResultPromises = batch.map((task) =>
|
|
729
|
+
executeTask(task, batchIsParallel).catch((err): TaskResult => {
|
|
730
|
+
console.log(chalk.yellow(` ⚠ ${task.id} threw unexpectedly: ${(err as Error).message}`));
|
|
731
|
+
return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
|
|
732
|
+
})
|
|
733
|
+
);
|
|
724
734
|
const batchResults = await Promise.all(batchResultPromises);
|
|
725
735
|
layerResults.push(...batchResults);
|
|
726
736
|
// Update cache after each batch so the next batch sees the exports.
|
|
@@ -838,8 +848,10 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
838
848
|
try {
|
|
839
849
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
840
850
|
const fileContent = stripCodeFences(raw);
|
|
851
|
+
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
841
852
|
await fs.ensureDir(path.dirname(fullPath));
|
|
842
853
|
await fs.writeFile(fullPath, fileContent, "utf-8");
|
|
854
|
+
getActiveLogger()?.fileWritten(item.file);
|
|
843
855
|
console.log(`${prefix}${existingContent ? chalk.yellow("~") : chalk.green("+")} ${chalk.bold(item.file)} ${chalk.green("✔")}`);
|
|
844
856
|
successCount++;
|
|
845
857
|
writtenFiles.push(item.file);
|
package/core/error-feedback.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { AIProvider } from "./spec-generator";
|
|
|
6
6
|
import { getCodeGenSystemPrompt } from "../prompts/codegen.prompt";
|
|
7
7
|
import { SpecDSL } from "./dsl-types";
|
|
8
8
|
import { buildDslContextSection } from "./dsl-extractor";
|
|
9
|
+
import { getActiveSnapshot } from "./run-snapshot";
|
|
9
10
|
|
|
10
11
|
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
11
12
|
|
|
@@ -242,6 +243,7 @@ Output ONLY the complete fixed file content. No markdown fences, no explanations
|
|
|
242
243
|
try {
|
|
243
244
|
const raw = await provider.generate(prompt, getCodeGenSystemPrompt());
|
|
244
245
|
const fixed = raw.replace(/^```\w*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
246
|
+
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
245
247
|
await fs.writeFile(fullPath, fixed, "utf-8");
|
|
246
248
|
results.push({ fixed: true, file, explanation: `Fixed ${fileErrors.length} error(s)` });
|
|
247
249
|
console.log(chalk.green(` ✔ Auto-fixed: ${file}`));
|
|
@@ -300,13 +302,22 @@ export async function runErrorFeedback(
|
|
|
300
302
|
console.log(chalk.gray(`\n [cycle ${cycle}/${maxCycles}] Type-check: ${buildCmd}`));
|
|
301
303
|
const buildResult = runCommand(buildCmd, workingDir);
|
|
302
304
|
if (!buildResult.success) {
|
|
303
|
-
// Detect tool crash
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
// Detect tool crash — the type-check binary itself threw an unhandled
|
|
306
|
+
// exception (e.g. vue-tsc / tsc version incompatibility).
|
|
307
|
+
//
|
|
308
|
+
// Two conditions must BOTH be true:
|
|
309
|
+
// 1. The output contains an uncaught JS error (ReferenceError, TypeError, …)
|
|
310
|
+
// 2. The stack trace has at least one frame inside node_modules,
|
|
311
|
+
// meaning the crash originated in the tool binary, not user code.
|
|
312
|
+
//
|
|
313
|
+
// TypeScript compilation errors from user code are formatted as
|
|
314
|
+
// "src/foo.ts:10:5 - error TS2345: …"
|
|
315
|
+
// and do NOT produce "at …" stack frames, so they are never misclassified.
|
|
316
|
+
const hasUncaughtError = /ReferenceError:|TypeError:|SyntaxError:/.test(buildResult.output);
|
|
317
|
+
const hasToolStackFrame = buildResult.output
|
|
318
|
+
.split("\n")
|
|
319
|
+
.some((l) => l.trim().startsWith("at ") && l.includes("node_modules"));
|
|
320
|
+
const isToolCrash = hasUncaughtError && hasToolStackFrame;
|
|
310
321
|
if (isToolCrash) {
|
|
311
322
|
console.log(chalk.yellow(` ⚠ Type-check tool crashed (possible version incompatibility). Skipping.`));
|
|
312
323
|
console.log(chalk.gray(` Tip: run \`${buildCmd}\` manually to investigate.`));
|