@spaceflow/review 2.0.0 → 4.0.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/CHANGELOG.md +20 -0
- package/README.md +136 -25
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/src/review-spec/review-spec.service.spec.ts +294 -5
- package/src/review-spec/review-spec.service.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@2.0.0...@spaceflow/review@3.0.0) (2026-04-13)
|
|
4
|
+
|
|
5
|
+
### 代码重构
|
|
6
|
+
|
|
7
|
+
* **review:** 规则级 includes 优先于文件级 includes,与 severity 优先级保持一致 ([1b03415](https://github.com/Lydanne/spaceflow/commit/1b0341587f4fcf6913b072a1b4731c8eb92114a2))
|
|
8
|
+
|
|
9
|
+
### 其他修改
|
|
10
|
+
|
|
11
|
+
* **review-summary:** released version 2.0.0 [no ci] ([4dbf0dd](https://github.com/Lydanne/spaceflow/commit/4dbf0dd37be9137e9b1a38163300be00b2ae851a))
|
|
12
|
+
|
|
13
|
+
## [2.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@1.0.0...@spaceflow/review@2.0.0) (2026-04-13)
|
|
14
|
+
|
|
15
|
+
### 代码重构
|
|
16
|
+
|
|
17
|
+
* **review:** 优化 Example 标题和描述解析逻辑 ([decb54f](https://github.com/Lydanne/spaceflow/commit/decb54f6832acaeddd83ccfb6a3439c47294f11d))
|
|
18
|
+
|
|
19
|
+
### 其他修改
|
|
20
|
+
|
|
21
|
+
* **review-summary:** released version 1.0.0 [no ci] ([742d53e](https://github.com/Lydanne/spaceflow/commit/742d53efb7f16e33c50d9b1c4b9e31a7c0e8da21))
|
|
22
|
+
|
|
3
23
|
## [1.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.83.0...@spaceflow/review@1.0.0) (2026-04-13)
|
|
4
24
|
|
|
5
25
|
### ⚠ BREAKING CHANGES
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@spaceflow/review)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
> Spaceflow AI 代码审查扩展,使用 LLM 对 PR 代码进行自动审查。支持 OpenAI、Claude Code、Gemini 等多种 LLM 模式。
|
|
6
|
+
> Spaceflow AI 代码审查扩展,使用 LLM 对 PR 代码进行自动审查。支持 OpenAI、Claude Code、Gemini、Open Code 等多种 LLM 模式。
|
|
7
7
|
|
|
8
8
|
## 安装
|
|
9
9
|
|
|
@@ -13,13 +13,17 @@ pnpm spaceflow install @spaceflow/review
|
|
|
13
13
|
|
|
14
14
|
## 功能特性
|
|
15
15
|
|
|
16
|
-
- **多 LLM 支持** — OpenAI、Claude Code、Gemini 可选
|
|
16
|
+
- **多 LLM 支持** — OpenAI、Claude Code、Gemini、Open Code 可选
|
|
17
17
|
- **行级评论** — 在 PR 中精确定位问题代码行
|
|
18
18
|
- **增量审查** — 多次运行时自动去重,追踪问题修复状态
|
|
19
19
|
- **删除代码分析** — 评估删除代码可能带来的风险
|
|
20
20
|
- **AI 生成 PR 描述** — 自动总结 PR 功能变更
|
|
21
21
|
- **审查规范** — 支持自定义 Markdown 格式的审查规则
|
|
22
22
|
- **远程规范引用** — 支持从远程仓库 URL 拉取审查规范
|
|
23
|
+
- **系统规则** — 不依赖 LLM 的静态检查(如单文件行数上限)
|
|
24
|
+
- **本地审查** — 审查未提交或暂存区的代码,无需 PR
|
|
25
|
+
- **MCP 工具** — 提供 `list_rules`、`get_rules_for_file` 等工具供 AI 编辑器使用
|
|
26
|
+
- **问题阻断** — 存在未解决问题时以非零退出码退出,适用于 CI 门禁
|
|
23
27
|
|
|
24
28
|
## 使用
|
|
25
29
|
|
|
@@ -38,29 +42,47 @@ spaceflow review -p 123 -l openai -vv
|
|
|
38
42
|
|
|
39
43
|
# 仅分析删除代码
|
|
40
44
|
spaceflow review -p 123 --deletion-only -l openai
|
|
45
|
+
|
|
46
|
+
# 本地审查(未提交的代码)
|
|
47
|
+
spaceflow review --local -l openai
|
|
48
|
+
|
|
49
|
+
# 仅审查暂存区代码
|
|
50
|
+
spaceflow review --local staged -l openai
|
|
51
|
+
|
|
52
|
+
# 仅刷新状态(同步 reactions、resolved 等)
|
|
53
|
+
spaceflow review --flush
|
|
54
|
+
|
|
55
|
+
# 存在 error 级别未解决问题时非零退出
|
|
56
|
+
spaceflow review -p 123 --fail-on-issues -l openai
|
|
41
57
|
```
|
|
42
58
|
|
|
43
59
|
## 命令行参数
|
|
44
60
|
|
|
45
|
-
| 参数 | 简写 | 说明
|
|
46
|
-
| --------------------------------- | ---- |
|
|
47
|
-
| `--pr-number <number>` | `-p` | PR 编号
|
|
48
|
-
| `--base <ref>` | `-b` | 基准分支/tag
|
|
49
|
-
| `--head <ref>` | | 目标分支/tag
|
|
50
|
-
| `--llm-mode <mode>` | `-l` | LLM 模式(`openai` / `claude-code` / `gemini`) |
|
|
51
|
-
| `--files <files...>` | `-f` | 仅审查指定文件
|
|
52
|
-
| `--commits <commits...>` | | 仅审查指定 commits
|
|
53
|
-
| `--includes <patterns...>` | `-i` | 文件 glob 过滤模式
|
|
54
|
-
| `--verbose [level]` | `-v` | 详细输出(1: 过程日志,2: 含提示词)
|
|
55
|
-
| `--dry-run` | `-d` | 仅打印将要执行的操作
|
|
56
|
-
| `--ci` | `-c` | 在 CI 环境中运行
|
|
57
|
-
| `--verify-fixes` | | 验证历史问题是否已修复
|
|
58
|
-
| `--no-verify-fixes` | | 禁用历史问题验证
|
|
59
|
-
| `--analyze-deletions` | | 分析删除代码影响
|
|
60
|
-
| `--deletion-only` | | 仅执行删除代码分析
|
|
61
|
-
| `--deletion-analysis-mode <mode>` | |
|
|
62
|
-
| `--generate-description` | | 使用 AI 生成 PR 功能描述
|
|
63
|
-
| `--output-format <format>` | `-o` | 输出格式(`markdown` / `terminal` / `json`)
|
|
61
|
+
| 参数 | 简写 | 说明 |
|
|
62
|
+
| --------------------------------- | ---- | ------------------------------------------------------------ |
|
|
63
|
+
| `--pr-number <number>` | `-p` | PR 编号 |
|
|
64
|
+
| `--base <ref>` | `-b` | 基准分支/tag |
|
|
65
|
+
| `--head <ref>` | | 目标分支/tag |
|
|
66
|
+
| `--llm-mode <mode>` | `-l` | LLM 模式(`openai` / `claude-code` / `gemini` / `open-code`) |
|
|
67
|
+
| `--files <files...>` | `-f` | 仅审查指定文件 |
|
|
68
|
+
| `--commits <commits...>` | | 仅审查指定 commits |
|
|
69
|
+
| `--includes <patterns...>` | `-i` | 文件 glob 过滤模式 |
|
|
70
|
+
| `--verbose [level]` | `-v` | 详细输出(1: 过程日志,2: 含提示词) |
|
|
71
|
+
| `--dry-run` | `-d` | 仅打印将要执行的操作 |
|
|
72
|
+
| `--ci` | `-c` | 在 CI 环境中运行 |
|
|
73
|
+
| `--verify-fixes` | | 验证历史问题是否已修复 |
|
|
74
|
+
| `--no-verify-fixes` | | 禁用历史问题验证 |
|
|
75
|
+
| `--analyze-deletions` | | 分析删除代码影响 |
|
|
76
|
+
| `--deletion-only` | | 仅执行删除代码分析 |
|
|
77
|
+
| `--deletion-analysis-mode <mode>` | | 删除分析 LLM 模式(`openai` / `claude-code`) |
|
|
78
|
+
| `--generate-description` | | 使用 AI 生成 PR 功能描述 |
|
|
79
|
+
| `--output-format <format>` | `-o` | 输出格式(`markdown` / `terminal` / `json`) |
|
|
80
|
+
| `--local [mode]` | | 本地审查模式(`uncommitted` / `staged`,默认 `uncommitted`) |
|
|
81
|
+
| `--no-local` | | 禁用本地模式 |
|
|
82
|
+
| `--show-all` | | 显示所有问题,不过滤非变更行的问题 |
|
|
83
|
+
| `--flush` | | 仅刷新状态(同步 reactions、resolved 等),不执行 LLM 审查 |
|
|
84
|
+
| `--event-action <action>` | | PR 事件类型(`opened` / `synchronize` / `closed` 等) |
|
|
85
|
+
| `--fail-on-issues [mode]` | | 未解决问题时非零退出(`off` / `warn` / `error` / `warn+error`,默认 `error`) |
|
|
64
86
|
|
|
65
87
|
## 配置
|
|
66
88
|
|
|
@@ -71,17 +93,72 @@ spaceflow review -p 123 --deletion-only -l openai
|
|
|
71
93
|
"review": {
|
|
72
94
|
"includes": ["*/**/*.ts", "!*/**/*.spec.*", "!*/**/*.config.*"],
|
|
73
95
|
"references": ["./references"],
|
|
96
|
+
"llmMode": "openai",
|
|
74
97
|
"generateDescription": true,
|
|
98
|
+
"autoUpdatePrTitle": false,
|
|
75
99
|
"lineComments": true,
|
|
76
100
|
"verifyFixes": true,
|
|
101
|
+
"verifyFixesConcurrency": 10,
|
|
77
102
|
"analyzeDeletions": false,
|
|
103
|
+
"deletionAnalysisMode": "openai",
|
|
104
|
+
"whenModifiedCode": ["function", "class"],
|
|
105
|
+
"rules": {
|
|
106
|
+
"no-console": "warn",
|
|
107
|
+
"no-any": "error"
|
|
108
|
+
},
|
|
78
109
|
"concurrency": 5,
|
|
79
110
|
"retries": 3,
|
|
80
|
-
"retryDelay": 1000
|
|
111
|
+
"retryDelay": 1000,
|
|
112
|
+
"invalidateChangedFiles": "invalidate",
|
|
113
|
+
"duplicateWorkflowResolved": "delete",
|
|
114
|
+
"autoApprove": false,
|
|
115
|
+
"failOnIssues": "off",
|
|
116
|
+
"systemRules": {
|
|
117
|
+
"maxLinesPerFile": [500, "warn"]
|
|
118
|
+
}
|
|
81
119
|
}
|
|
82
120
|
}
|
|
83
121
|
```
|
|
84
122
|
|
|
123
|
+
### 配置项说明
|
|
124
|
+
|
|
125
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
126
|
+
| ---------------------------- | ---------------------------------------- | -------------- | ---------------------------------------------------------------------- |
|
|
127
|
+
| `includes` | `string[]` | — | 文件 glob 过滤模式 |
|
|
128
|
+
| `references` | `string[]` | — | 审查规范来源(本地路径或远程仓库 URL) |
|
|
129
|
+
| `llmMode` | `string` | `"openai"` | 默认 LLM 模式 |
|
|
130
|
+
| `generateDescription` | `boolean` | `false` | AI 生成 PR 功能描述 |
|
|
131
|
+
| `autoUpdatePrTitle` | `boolean` | `false` | 自动更新 PR 标题 |
|
|
132
|
+
| `lineComments` | `boolean` | `false` | 在 PR 中发布行级评论 |
|
|
133
|
+
| `verifyFixes` | `boolean` | `false` | 验证历史问题是否已修复 |
|
|
134
|
+
| `verifyFixesConcurrency` | `number` | `10` | 验证并发数 |
|
|
135
|
+
| `analyzeDeletions` | `boolean \| "ci" \| "pr" \| "terminal"` | `false` | 删除代码分析(`true`: 始终启用,`"ci"`: 仅 CI,`"pr"`: 仅 PR,`"terminal"`: 仅终端) |
|
|
136
|
+
| `deletionAnalysisMode` | `string` | `"openai"` | 删除分析 LLM 模式 |
|
|
137
|
+
| `whenModifiedCode` | `string[]` | — | 代码结构过滤(`"function"` / `"class"` / `"interface"` / `"type"` / `"method"`) |
|
|
138
|
+
| `rules` | `Record<string, "off" \| "warn" \| "error">` | — | 逐规则严重级别覆盖 |
|
|
139
|
+
| `concurrency` | `number` | `5` | LLM 并发审查数 |
|
|
140
|
+
| `retries` | `number` | `0` | 失败重试次数 |
|
|
141
|
+
| `retryDelay` | `number` | `1000` | 重试间隔(ms) |
|
|
142
|
+
| `invalidateChangedFiles` | `"invalidate" \| "keep" \| "off"` | `"invalidate"` | 变更文件历史问题处理策略 |
|
|
143
|
+
| `duplicateWorkflowResolved` | `"off" \| "skip" \| "delete"` | `"delete"` | 重复 workflow 处理策略 |
|
|
144
|
+
| `autoApprove` | `boolean` | `false` | 所有问题解决后自动提交 APPROVE review |
|
|
145
|
+
| `failOnIssues` | `"off" \| "warn" \| "error" \| "warn+error"` | `"off"` | 问题阻断模式 |
|
|
146
|
+
| `systemRules` | `object` | — | 系统规则(不依赖 LLM 的静态检查) |
|
|
147
|
+
| `systemRules.maxLinesPerFile`| `[number, "off" \| "warn" \| "error"]` | — | 单文件最大审查行数,超过时跳过 LLM 并生成系统问题 |
|
|
148
|
+
|
|
149
|
+
## MCP 工具
|
|
150
|
+
|
|
151
|
+
本扩展提供以下 MCP 工具,可在 AI 编辑器(如 Cursor、Windsurf)中使用:
|
|
152
|
+
|
|
153
|
+
| 工具名 | 说明 | 参数 |
|
|
154
|
+
| ---------------------- | ------------------------------------------ | ----------------------------------------------- |
|
|
155
|
+
| `list_rules` | 列出所有审查规则 | — |
|
|
156
|
+
| `get_rules_for_file` | 获取指定文件适用的审查规则 | `filePath`, `includeExamples?` |
|
|
157
|
+
| `get_rule_detail` | 获取单条规则的详细信息 | `ruleId` |
|
|
158
|
+
| `get_rules_from_dir` | 从指定目录加载审查规则 | `dirPath`, `includeExamples?` |
|
|
159
|
+
|
|
160
|
+
规则搜索目录包括:`review.references` 配置路径、`.claude/skills`、`.cursor/skills`、`review-specs`。
|
|
161
|
+
|
|
85
162
|
## PR 标题参数
|
|
86
163
|
|
|
87
164
|
支持在 PR 标题末尾添加参数覆盖默认配置:
|
|
@@ -109,15 +186,49 @@ jobs:
|
|
|
109
186
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
110
187
|
```
|
|
111
188
|
|
|
189
|
+
### CI 门禁示例
|
|
190
|
+
|
|
191
|
+
存在 `error` 级别未解决问题时阻止合并:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
- run: pnpm spaceflow review --ci -l openai --fail-on-issues
|
|
195
|
+
env:
|
|
196
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
197
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
198
|
+
```
|
|
199
|
+
|
|
112
200
|
## 环境变量
|
|
113
201
|
|
|
202
|
+
### Git Provider
|
|
203
|
+
|
|
114
204
|
| 变量 | 说明 |
|
|
115
205
|
| ------------------- | ----------------------------- |
|
|
116
206
|
| `GITHUB_TOKEN` | GitHub API Token |
|
|
117
207
|
| `GITHUB_REPOSITORY` | 仓库名称(`owner/repo` 格式) |
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
208
|
+
| `GITEA_TOKEN` | Gitea API Token |
|
|
209
|
+
| `GITEA_REPOSITORY` | Gitea 仓库名称 |
|
|
210
|
+
| `GITLAB_TOKEN` | GitLab API Token |
|
|
211
|
+
| `GITLAB_REPOSITORY` | GitLab 仓库名称 |
|
|
212
|
+
|
|
213
|
+
### OpenAI
|
|
214
|
+
|
|
215
|
+
| 变量 | 说明 |
|
|
216
|
+
| ----------------- | --------------- |
|
|
217
|
+
| `OPENAI_BASE_URL` | OpenAI API 地址 |
|
|
218
|
+
| `OPENAI_API_KEY` | OpenAI API Key |
|
|
219
|
+
| `OPENAI_MODEL` | OpenAI 模型名称 |
|
|
220
|
+
|
|
221
|
+
### Claude Code
|
|
222
|
+
|
|
223
|
+
| 变量 | 说明 |
|
|
224
|
+
| ------------------- | ---------------- |
|
|
225
|
+
| `ANTHROPIC_API_KEY` | Anthropic API Key |
|
|
226
|
+
|
|
227
|
+
### Gemini
|
|
228
|
+
|
|
229
|
+
| 变量 | 说明 |
|
|
230
|
+
| ------------------ | -------------- |
|
|
231
|
+
| `GEMINI_API_KEY` | Gemini API Key |
|
|
121
232
|
|
|
122
233
|
## 许可证
|
|
123
234
|
|
package/dist/index.js
CHANGED
|
@@ -988,12 +988,12 @@ class ReviewSpecService {
|
|
|
988
988
|
* 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
|
|
989
989
|
* 支持 `added|`/`modified|`/`deleted|` 前缀语法
|
|
990
990
|
*/ filterIssuesByIncludes(issues, specs, changedFiles) {
|
|
991
|
-
// 构建
|
|
991
|
+
// 构建 rule.id -> includes 的映射(规则级优先,文件级兜底)
|
|
992
992
|
const specIncludesMap = new Map();
|
|
993
993
|
for (const spec of specs){
|
|
994
|
-
// 从规则 ID 前缀推断 spec filename
|
|
995
994
|
for (const rule of spec.rules){
|
|
996
|
-
|
|
995
|
+
// 规则级 includes 覆盖文件级 includes,与 severity 优先级一致
|
|
996
|
+
specIncludesMap.set(rule.id, rule.includes ?? spec.includes);
|
|
997
997
|
}
|
|
998
998
|
}
|
|
999
999
|
return issues.filter((issue)=>{
|
package/package.json
CHANGED
|
@@ -1674,8 +1674,8 @@ const bad_name = 1;
|
|
|
1674
1674
|
});
|
|
1675
1675
|
});
|
|
1676
1676
|
|
|
1677
|
-
describe("filterIssuesByIncludes -
|
|
1678
|
-
it("should use rule-level includes
|
|
1677
|
+
describe("filterIssuesByIncludes - includes priority", () => {
|
|
1678
|
+
it("should use rule-level includes over file-level includes", () => {
|
|
1679
1679
|
const specs = [
|
|
1680
1680
|
{
|
|
1681
1681
|
filename: "nest.md",
|
|
@@ -1706,12 +1706,301 @@ const bad_name = 1;
|
|
|
1706
1706
|
];
|
|
1707
1707
|
const issues = [
|
|
1708
1708
|
{ file: "user.model.ts", ruleId: "JsTs.Nest.Model" },
|
|
1709
|
+
{ file: "order.model.ts", ruleId: "JsTs.Nest.Model" },
|
|
1709
1710
|
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1711
|
+
{ file: "order.controller.ts", ruleId: "JsTs.Nest" },
|
|
1712
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1713
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest" },
|
|
1714
|
+
{ file: "user.model.ts", ruleId: "JsTs.Nest" }, // model 文件但 ruleId 是 Nest(文件级)
|
|
1710
1715
|
];
|
|
1711
1716
|
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1712
|
-
//
|
|
1713
|
-
|
|
1714
|
-
|
|
1717
|
+
// JsTs.Nest.Model → 规则级 *.model.ts,user.model.ts / order.model.ts 匹配 → 保留
|
|
1718
|
+
// JsTs.Nest → 文件级 *.controller.ts,user.controller.ts / order.controller.ts 匹配 → 保留
|
|
1719
|
+
// user.service.ts / user.module.ts 不匹配文件级 *.controller.ts → 过滤
|
|
1720
|
+
// user.model.ts(ruleId=Nest) → 文件级 *.controller.ts,不匹配 → 过滤
|
|
1721
|
+
expect(result).toHaveLength(4);
|
|
1722
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1723
|
+
"user.model.ts",
|
|
1724
|
+
"order.model.ts",
|
|
1725
|
+
"user.controller.ts",
|
|
1726
|
+
"order.controller.ts",
|
|
1727
|
+
]);
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
it("should fall back to file-level includes when rule has no includes", () => {
|
|
1731
|
+
const specs = [
|
|
1732
|
+
{
|
|
1733
|
+
filename: "nest.md",
|
|
1734
|
+
extensions: ["ts"],
|
|
1735
|
+
type: "nest",
|
|
1736
|
+
content: "",
|
|
1737
|
+
overrides: [],
|
|
1738
|
+
severity: "error" as const,
|
|
1739
|
+
includes: ["*.controller.ts"],
|
|
1740
|
+
rules: [
|
|
1741
|
+
{
|
|
1742
|
+
id: "JsTs.Nest",
|
|
1743
|
+
title: "Nest",
|
|
1744
|
+
description: "",
|
|
1745
|
+
examples: [],
|
|
1746
|
+
overrides: [],
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
id: "JsTs.Nest.DirStructure",
|
|
1750
|
+
title: "DirStructure",
|
|
1751
|
+
description: "",
|
|
1752
|
+
examples: [],
|
|
1753
|
+
overrides: [],
|
|
1754
|
+
// 无 rule.includes → 回退到 spec.includes
|
|
1755
|
+
},
|
|
1756
|
+
],
|
|
1757
|
+
},
|
|
1758
|
+
];
|
|
1759
|
+
const issues = [
|
|
1760
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1761
|
+
{ file: "order.controller.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1762
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1763
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1764
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1765
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1766
|
+
];
|
|
1767
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1768
|
+
// DirStructure 无规则级 includes → 回退文件级 *.controller.ts
|
|
1769
|
+
// user.controller.ts / order.controller.ts 匹配 → 保留
|
|
1770
|
+
// user.service.ts / user.module.ts 不匹配 → 过滤
|
|
1771
|
+
// JsTs.Nest 同样回退文件级,user.controller.ts 匹配 → 保留,user.service.ts 不匹配 → 过滤
|
|
1772
|
+
expect(result).toHaveLength(3);
|
|
1773
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1774
|
+
"user.controller.ts",
|
|
1775
|
+
"order.controller.ts",
|
|
1776
|
+
"user.controller.ts",
|
|
1777
|
+
]);
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
it("should use rule-level includes even when it narrows the file-level scope", () => {
|
|
1781
|
+
const specs = [
|
|
1782
|
+
{
|
|
1783
|
+
filename: "js&ts.md",
|
|
1784
|
+
extensions: ["ts"],
|
|
1785
|
+
type: "base",
|
|
1786
|
+
content: "",
|
|
1787
|
+
overrides: [],
|
|
1788
|
+
severity: "error" as const,
|
|
1789
|
+
includes: ["*.ts"], // 文件级:所有 ts 文件
|
|
1790
|
+
rules: [
|
|
1791
|
+
{
|
|
1792
|
+
id: "JsTs.Base",
|
|
1793
|
+
title: "Base",
|
|
1794
|
+
description: "",
|
|
1795
|
+
examples: [],
|
|
1796
|
+
overrides: [],
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
id: "JsTs.Base.TestRule",
|
|
1800
|
+
title: "TestRule",
|
|
1801
|
+
description: "",
|
|
1802
|
+
examples: [],
|
|
1803
|
+
overrides: [],
|
|
1804
|
+
includes: ["*.spec.ts"], // 规则级:仅 spec 文件(收窄)
|
|
1805
|
+
},
|
|
1806
|
+
],
|
|
1807
|
+
},
|
|
1808
|
+
];
|
|
1809
|
+
const issues = [
|
|
1810
|
+
{ file: "app.spec.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1811
|
+
{ file: "utils.spec.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1812
|
+
{ file: "app.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1813
|
+
{ file: "utils.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1814
|
+
{ file: "app.ts", ruleId: "JsTs.Base" },
|
|
1815
|
+
{ file: "utils.ts", ruleId: "JsTs.Base" },
|
|
1816
|
+
{ file: "app.spec.ts", ruleId: "JsTs.Base" },
|
|
1817
|
+
];
|
|
1818
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1819
|
+
// JsTs.Base.TestRule → 规则级 *.spec.ts,app.spec.ts / utils.spec.ts 匹配 → 保留
|
|
1820
|
+
// app.ts / utils.ts 不匹配 *.spec.ts → 过滤
|
|
1821
|
+
// JsTs.Base → 文件级 *.ts,app.ts / utils.ts / app.spec.ts 匹配 → 保留
|
|
1822
|
+
expect(result).toHaveLength(5);
|
|
1823
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1824
|
+
"app.spec.ts",
|
|
1825
|
+
"utils.spec.ts",
|
|
1826
|
+
"app.ts",
|
|
1827
|
+
"utils.ts",
|
|
1828
|
+
"app.spec.ts",
|
|
1829
|
+
]);
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
it("should use rule-level includes even when it widens the file-level scope", () => {
|
|
1833
|
+
const specs = [
|
|
1834
|
+
{
|
|
1835
|
+
filename: "nest.md",
|
|
1836
|
+
extensions: ["ts"],
|
|
1837
|
+
type: "nest",
|
|
1838
|
+
content: "",
|
|
1839
|
+
overrides: [],
|
|
1840
|
+
severity: "error" as const,
|
|
1841
|
+
includes: ["*.controller.ts"], // 文件级:仅 controller
|
|
1842
|
+
rules: [
|
|
1843
|
+
{
|
|
1844
|
+
id: "JsTs.Nest",
|
|
1845
|
+
title: "Nest",
|
|
1846
|
+
description: "",
|
|
1847
|
+
examples: [],
|
|
1848
|
+
overrides: [],
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
id: "JsTs.Nest.AllFiles",
|
|
1852
|
+
title: "AllFiles",
|
|
1853
|
+
description: "",
|
|
1854
|
+
examples: [],
|
|
1855
|
+
overrides: [],
|
|
1856
|
+
includes: ["*.ts"], // 规则级:所有 ts 文件(放宽)
|
|
1857
|
+
},
|
|
1858
|
+
],
|
|
1859
|
+
},
|
|
1860
|
+
];
|
|
1861
|
+
const issues = [
|
|
1862
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest.AllFiles" },
|
|
1863
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest.AllFiles" },
|
|
1864
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest.AllFiles" },
|
|
1865
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1866
|
+
{ file: "order.controller.ts", ruleId: "JsTs.Nest" },
|
|
1867
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1868
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest" },
|
|
1869
|
+
];
|
|
1870
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1871
|
+
// JsTs.Nest.AllFiles → 规则级 *.ts,全部匹配 → 保留 3 个
|
|
1872
|
+
// JsTs.Nest → 文件级 *.controller.ts,user.controller.ts / order.controller.ts 匹配 → 保留
|
|
1873
|
+
// user.service.ts / user.module.ts 不匹配 → 过滤
|
|
1874
|
+
expect(result).toHaveLength(5);
|
|
1875
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1876
|
+
"user.service.ts",
|
|
1877
|
+
"user.controller.ts",
|
|
1878
|
+
"user.module.ts",
|
|
1879
|
+
"user.controller.ts",
|
|
1880
|
+
"order.controller.ts",
|
|
1881
|
+
]);
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
it("should support status prefix in rule-level includes", () => {
|
|
1885
|
+
const specs = [
|
|
1886
|
+
{
|
|
1887
|
+
filename: "nest.md",
|
|
1888
|
+
extensions: ["ts"],
|
|
1889
|
+
type: "nest",
|
|
1890
|
+
content: "",
|
|
1891
|
+
overrides: [],
|
|
1892
|
+
severity: "error" as const,
|
|
1893
|
+
includes: ["*.controller.ts"], // 文件级:无前缀
|
|
1894
|
+
rules: [
|
|
1895
|
+
{
|
|
1896
|
+
id: "JsTs.Nest",
|
|
1897
|
+
title: "Nest",
|
|
1898
|
+
description: "",
|
|
1899
|
+
examples: [],
|
|
1900
|
+
overrides: [],
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
id: "JsTs.Nest.Model",
|
|
1904
|
+
title: "Model",
|
|
1905
|
+
description: "",
|
|
1906
|
+
examples: [],
|
|
1907
|
+
overrides: [],
|
|
1908
|
+
includes: ["added|*.model.ts", "modified|*.dto.ts"], // 规则级:added 的 model + modified 的 dto
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
},
|
|
1912
|
+
];
|
|
1913
|
+
const changedFiles = {
|
|
1914
|
+
getStatus: (file: string) => {
|
|
1915
|
+
if (file === "user.model.ts") return "added";
|
|
1916
|
+
if (file === "order.model.ts") return "modified";
|
|
1917
|
+
if (file === "user.dto.ts") return "modified";
|
|
1918
|
+
if (file === "order.dto.ts") return "added";
|
|
1919
|
+
return "modified";
|
|
1920
|
+
},
|
|
1921
|
+
} as any;
|
|
1922
|
+
const issues = [
|
|
1923
|
+
{ file: "user.model.ts", ruleId: "JsTs.Nest.Model" }, // added + *.model.ts → 匹配
|
|
1924
|
+
{ file: "order.model.ts", ruleId: "JsTs.Nest.Model" }, // modified + *.model.ts → 不匹配 added|
|
|
1925
|
+
{ file: "user.dto.ts", ruleId: "JsTs.Nest.Model" }, // modified + *.dto.ts → 匹配 modified|
|
|
1926
|
+
{ file: "order.dto.ts", ruleId: "JsTs.Nest.Model" }, // added + *.dto.ts → 不匹配 modified|
|
|
1927
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" }, // 文件级 *.controller.ts → 匹配
|
|
1928
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" }, // 文件级 *.controller.ts → 不匹配
|
|
1929
|
+
];
|
|
1930
|
+
const result = service.filterIssuesByIncludes(issues, specs, changedFiles);
|
|
1931
|
+
expect(result).toHaveLength(3);
|
|
1932
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1933
|
+
"user.model.ts",
|
|
1934
|
+
"user.dto.ts",
|
|
1935
|
+
"user.controller.ts",
|
|
1936
|
+
]);
|
|
1937
|
+
|
|
1938
|
+
// 全部 modified 时,added|*.model.ts 不匹配,modified|*.dto.ts 匹配
|
|
1939
|
+
const changedFiles2 = {
|
|
1940
|
+
getStatus: () => "modified",
|
|
1941
|
+
} as any;
|
|
1942
|
+
const result2 = service.filterIssuesByIncludes(issues, specs, changedFiles2);
|
|
1943
|
+
// user.model.ts: modified, added|*.model.ts 不匹配 → 过滤
|
|
1944
|
+
// order.model.ts: modified, added|*.model.ts 不匹配 → 过滤
|
|
1945
|
+
// user.dto.ts: modified, modified|*.dto.ts 匹配 → 保留
|
|
1946
|
+
// order.dto.ts: modified, modified|*.dto.ts 匹配 → 保留
|
|
1947
|
+
// user.controller.ts: 文件级匹配 → 保留
|
|
1948
|
+
// user.service.ts: 文件级不匹配 → 过滤
|
|
1949
|
+
expect(result2).toHaveLength(3);
|
|
1950
|
+
expect(result2.map((i) => i.file)).toEqual([
|
|
1951
|
+
"user.dto.ts",
|
|
1952
|
+
"order.dto.ts",
|
|
1953
|
+
"user.controller.ts",
|
|
1954
|
+
]);
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
it("should use empty rule-level includes to remove file-level restriction", () => {
|
|
1958
|
+
const specs = [
|
|
1959
|
+
{
|
|
1960
|
+
filename: "nest.md",
|
|
1961
|
+
extensions: ["ts"],
|
|
1962
|
+
type: "nest",
|
|
1963
|
+
content: "",
|
|
1964
|
+
overrides: [],
|
|
1965
|
+
severity: "error" as const,
|
|
1966
|
+
includes: ["*.controller.ts"], // 文件级:仅 controller
|
|
1967
|
+
rules: [
|
|
1968
|
+
{
|
|
1969
|
+
id: "JsTs.Nest",
|
|
1970
|
+
title: "Nest",
|
|
1971
|
+
description: "",
|
|
1972
|
+
examples: [],
|
|
1973
|
+
overrides: [],
|
|
1974
|
+
},
|
|
1975
|
+
{
|
|
1976
|
+
id: "JsTs.Nest.Global",
|
|
1977
|
+
title: "Global",
|
|
1978
|
+
description: "",
|
|
1979
|
+
examples: [],
|
|
1980
|
+
overrides: [],
|
|
1981
|
+
includes: [], // 规则级:空数组 = 无限制(覆盖文件级)
|
|
1982
|
+
},
|
|
1983
|
+
],
|
|
1984
|
+
},
|
|
1985
|
+
];
|
|
1986
|
+
const issues = [
|
|
1987
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest.Global" },
|
|
1988
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest.Global" },
|
|
1989
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest.Global" },
|
|
1990
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1991
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1992
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest" },
|
|
1993
|
+
];
|
|
1994
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1995
|
+
// JsTs.Nest.Global → 规则级 includes=[] (空=无限制),全部保留 3 个
|
|
1996
|
+
// JsTs.Nest → 文件级 *.controller.ts,仅 user.controller.ts 匹配 → 保留
|
|
1997
|
+
expect(result).toHaveLength(4);
|
|
1998
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1999
|
+
"user.service.ts",
|
|
2000
|
+
"user.controller.ts",
|
|
2001
|
+
"user.module.ts",
|
|
2002
|
+
"user.controller.ts",
|
|
2003
|
+
]);
|
|
1715
2004
|
});
|
|
1716
2005
|
});
|
|
1717
2006
|
|
|
@@ -799,12 +799,12 @@ export class ReviewSpecService {
|
|
|
799
799
|
specs: ReviewSpec[],
|
|
800
800
|
changedFiles?: ChangedFileCollection,
|
|
801
801
|
): T[] {
|
|
802
|
-
// 构建
|
|
802
|
+
// 构建 rule.id -> includes 的映射(规则级优先,文件级兜底)
|
|
803
803
|
const specIncludesMap = new Map<string, string[]>();
|
|
804
804
|
for (const spec of specs) {
|
|
805
|
-
// 从规则 ID 前缀推断 spec filename
|
|
806
805
|
for (const rule of spec.rules) {
|
|
807
|
-
|
|
806
|
+
// 规则级 includes 覆盖文件级 includes,与 severity 优先级一致
|
|
807
|
+
specIncludesMap.set(rule.id, rule.includes ?? spec.includes);
|
|
808
808
|
}
|
|
809
809
|
}
|
|
810
810
|
|