@spaceflow/review 0.83.0 → 2.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 +27 -0
- package/dist/index.js +214 -202
- package/package.json +1 -1
- package/src/changed-file-collection.ts +11 -0
- package/src/mcp/index.ts +23 -16
- package/src/prompt/specs-section.ts +47 -0
- package/src/review-includes-filter.spec.ts +83 -0
- package/src/review-includes-filter.ts +80 -0
- package/src/review-llm.ts +6 -8
- package/src/review-spec/review-spec.service.spec.ts +266 -11
- package/src/review-spec/review-spec.service.ts +75 -56
- package/src/review-spec/types.ts +8 -2
- package/src/review.service.ts +7 -3
- package/dist/551.js +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.83.0...@spaceflow/review@1.0.0) (2026-04-13)
|
|
4
|
+
|
|
5
|
+
### ⚠ BREAKING CHANGES
|
|
6
|
+
|
|
7
|
+
* **review:** 重构 example 数据结构,支持 `### Example:` 分组和 `#### Good:/Bad:` 标题语法
|
|
8
|
+
|
|
9
|
+
### 新特性
|
|
10
|
+
|
|
11
|
+
* **review:** 新增 includes 变更类型前缀语法支持 ([2f5655f](https://github.com/Lydanne/spaceflow/commit/2f5655fc414828ea3a0269fba04611d2bf2591a9))
|
|
12
|
+
* **review:** 重构 example 数据结构,支持 `### Example:` 分组和 `#### Good:/Bad:` 标题语法 ([e45bd5a](https://github.com/Lydanne/spaceflow/commit/e45bd5aab5e317b8f70a8b107c17aab6548c3296))
|
|
13
|
+
|
|
14
|
+
### 其他修改
|
|
15
|
+
|
|
16
|
+
* **review-summary:** released version 0.52.0 [no ci] ([1d0cdac](https://github.com/Lydanne/spaceflow/commit/1d0cdacdbe12db98399e69fd53bbfdd23825cc10))
|
|
17
|
+
|
|
18
|
+
## [0.83.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.82.0...@spaceflow/review@0.83.0) (2026-04-10)
|
|
19
|
+
|
|
20
|
+
### 代码重构
|
|
21
|
+
|
|
22
|
+
* **review:** 在 showAll=false 时过滤 merge commit 并屏蔽非本次 PR 的变更行 hash ([4edd6e2](https://github.com/Lydanne/spaceflow/commit/4edd6e285085c5fe22238eff273e7a3a683e4dc0))
|
|
23
|
+
|
|
24
|
+
### 其他修改
|
|
25
|
+
|
|
26
|
+
* **review-summary:** released version 0.51.0 [no ci] ([a76f9a4](https://github.com/Lydanne/spaceflow/commit/a76f9a4e0de05f209318fa3527532c5a54ada2bc))
|
|
27
|
+
* **scripts:** released version 0.34.0 [no ci] ([9bb3509](https://github.com/Lydanne/spaceflow/commit/9bb35096a2b1c42d6dd9b1836128249c622d22d6))
|
|
28
|
+
* **shell:** released version 0.34.0 [no ci] ([6a1638f](https://github.com/Lydanne/spaceflow/commit/6a1638fe69b0f086ddde4075bee2a76604f27ccb))
|
|
29
|
+
|
|
3
30
|
## [0.82.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.81.0...@spaceflow/review@0.82.0) (2026-04-09)
|
|
4
31
|
|
|
5
32
|
### 代码重构
|
package/dist/index.js
CHANGED
|
@@ -3,34 +3,10 @@ import { access, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises
|
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, normalize, relative } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { execFileSync, execSync, spawn } from "child_process";
|
|
6
|
-
import
|
|
6
|
+
import micromatch from "micromatch";
|
|
7
7
|
import { existsSync } from "fs";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var __webpack_module_cache__ = {};
|
|
11
|
-
|
|
12
|
-
// The require function
|
|
13
|
-
function __webpack_require__(moduleId) {
|
|
14
|
-
|
|
15
|
-
// Check if module is in cache
|
|
16
|
-
var cachedModule = __webpack_module_cache__[moduleId];
|
|
17
|
-
if (cachedModule !== undefined) {
|
|
18
|
-
return cachedModule.exports;
|
|
19
|
-
}
|
|
20
|
-
// Create a new module (and put it into the cache)
|
|
21
|
-
var module = (__webpack_module_cache__[moduleId] = {
|
|
22
|
-
exports: {}
|
|
23
|
-
});
|
|
24
|
-
// Execute the module function
|
|
25
|
-
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
26
|
-
|
|
27
|
-
// Return the exports of the module
|
|
28
|
-
return module.exports;
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// expose the modules object (__webpack_modules__)
|
|
33
|
-
__webpack_require__.m = __webpack_modules__;
|
|
8
|
+
// The require scope
|
|
9
|
+
var __webpack_require__ = {};
|
|
34
10
|
|
|
35
11
|
// webpack/runtime/define_property_getters
|
|
36
12
|
(() => {
|
|
@@ -42,95 +18,9 @@ __webpack_require__.d = (exports, definition) => {
|
|
|
42
18
|
}
|
|
43
19
|
};
|
|
44
20
|
})();
|
|
45
|
-
// webpack/runtime/ensure_chunk
|
|
46
|
-
(() => {
|
|
47
|
-
__webpack_require__.f = {};
|
|
48
|
-
// This file contains only the entry chunk.
|
|
49
|
-
// The chunk loading function for additional chunks
|
|
50
|
-
__webpack_require__.e = (chunkId) => {
|
|
51
|
-
return Promise.all(
|
|
52
|
-
Object.keys(__webpack_require__.f).reduce((promises, key) => {
|
|
53
|
-
__webpack_require__.f[key](chunkId, promises);
|
|
54
|
-
return promises;
|
|
55
|
-
}, [])
|
|
56
|
-
);
|
|
57
|
-
};
|
|
58
|
-
})();
|
|
59
|
-
// webpack/runtime/get javascript chunk filename
|
|
60
|
-
(() => {
|
|
61
|
-
// This function allow to reference chunks
|
|
62
|
-
__webpack_require__.u = (chunkId) => {
|
|
63
|
-
// return url for filenames not based on template
|
|
64
|
-
|
|
65
|
-
// return url for filenames based on template
|
|
66
|
-
return "" + chunkId + ".js"
|
|
67
|
-
}
|
|
68
|
-
})();
|
|
69
21
|
// webpack/runtime/has_own_property
|
|
70
22
|
(() => {
|
|
71
23
|
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
|
72
|
-
})();
|
|
73
|
-
// webpack/runtime/public_path
|
|
74
|
-
(() => {
|
|
75
|
-
__webpack_require__.p = "";
|
|
76
|
-
})();
|
|
77
|
-
// webpack/runtime/module_chunk_loading
|
|
78
|
-
(() => {
|
|
79
|
-
// no BaseURI
|
|
80
|
-
// object to store loaded and loading chunks
|
|
81
|
-
// undefined = chunk not loaded, null = chunk preloaded/prefetched
|
|
82
|
-
// [resolve, Promise] = chunk loading, 0 = chunk loaded
|
|
83
|
-
var installedChunks = {"410": 0,};
|
|
84
|
-
var installChunk = (data) => {
|
|
85
|
-
var __rspack_esm_ids = data.__rspack_esm_ids;
|
|
86
|
-
var __webpack_modules__ = data.__webpack_modules__;
|
|
87
|
-
var __rspack_esm_runtime = data.__rspack_esm_runtime;
|
|
88
|
-
// add "modules" to the modules object,
|
|
89
|
-
// then flag all "ids" as loaded and fire callback
|
|
90
|
-
var moduleId, chunkId, i = 0;
|
|
91
|
-
for (moduleId in __webpack_modules__) {
|
|
92
|
-
if (__webpack_require__.o(__webpack_modules__, moduleId)) {
|
|
93
|
-
__webpack_require__.m[moduleId] = __webpack_modules__[moduleId];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (__rspack_esm_runtime) __rspack_esm_runtime(__webpack_require__);
|
|
97
|
-
for (; i < __rspack_esm_ids.length; i++) {
|
|
98
|
-
chunkId = __rspack_esm_ids[i];
|
|
99
|
-
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
|
|
100
|
-
installedChunks[chunkId][0]();
|
|
101
|
-
}
|
|
102
|
-
installedChunks[__rspack_esm_ids[i]] = 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
};
|
|
106
|
-
__webpack_require__.f.j = function (chunkId, promises) {
|
|
107
|
-
// import() chunk loading for javascript
|
|
108
|
-
var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
|
|
109
|
-
if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
110
|
-
// a Promise means "currently loading".
|
|
111
|
-
if (installedChunkData) {
|
|
112
|
-
promises.push(installedChunkData[1]);
|
|
113
|
-
} else {
|
|
114
|
-
if (true) {
|
|
115
|
-
// setup Promise in chunk cache
|
|
116
|
-
var promise = import("./" + __webpack_require__.u(chunkId)).then(installChunk, (e) => {
|
|
117
|
-
if (installedChunks[chunkId] !== 0) installedChunks[chunkId] = undefined;
|
|
118
|
-
throw e;
|
|
119
|
-
});
|
|
120
|
-
var promise = Promise.race([promise, new Promise((resolve) => {
|
|
121
|
-
installedChunkData = installedChunks[chunkId] = [resolve];
|
|
122
|
-
})]);
|
|
123
|
-
promises.push(installedChunkData[1] = promise);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// no external install chunk
|
|
130
|
-
// no on chunks loaded
|
|
131
|
-
// no HMR
|
|
132
|
-
// no HMR manifest
|
|
133
|
-
|
|
134
24
|
})();
|
|
135
25
|
var __webpack_exports__ = {};
|
|
136
26
|
|
|
@@ -262,14 +152,14 @@ const CODE_BLOCK_TYPES = [
|
|
|
262
152
|
const fileStatus = STATUS_ALIAS[file.status?.toLowerCase() ?? ""] ?? "modified";
|
|
263
153
|
if (!filename) return false;
|
|
264
154
|
// 最终排除:命中排除模式的文件直接过滤掉
|
|
265
|
-
if (negativeGlobs.length > 0 &&
|
|
155
|
+
if (negativeGlobs.length > 0 && micromatch.isMatch(filename, negativeGlobs, {
|
|
266
156
|
matchBase: true
|
|
267
157
|
})) {
|
|
268
158
|
console.log(`[filterFilesByIncludes] ${filename} excluded by negativeGlobs`);
|
|
269
159
|
return false;
|
|
270
160
|
}
|
|
271
161
|
// 正向匹配:无前缀 glob
|
|
272
|
-
if (plainGlobs.length > 0 &&
|
|
162
|
+
if (plainGlobs.length > 0 && micromatch.isMatch(filename, plainGlobs, {
|
|
273
163
|
matchBase: true
|
|
274
164
|
})) {
|
|
275
165
|
console.log(`[filterFilesByIncludes] ${filename} matched plainGlobs`);
|
|
@@ -285,10 +175,10 @@ const CODE_BLOCK_TYPES = [
|
|
|
285
175
|
const positiveGlobs = matchingStatusGlobs.filter((g)=>!g.startsWith("!"));
|
|
286
176
|
const negativeStatusGlobs = matchingStatusGlobs.filter((g)=>g.startsWith("!")).map((g)=>g.slice(1));
|
|
287
177
|
if (positiveGlobs.length > 0) {
|
|
288
|
-
const matchesPositive =
|
|
178
|
+
const matchesPositive = micromatch.isMatch(filename, positiveGlobs, {
|
|
289
179
|
matchBase: true
|
|
290
180
|
});
|
|
291
|
-
const matchesNegative = negativeStatusGlobs.length > 0 &&
|
|
181
|
+
const matchesNegative = negativeStatusGlobs.length > 0 && micromatch.isMatch(filename, negativeStatusGlobs, {
|
|
292
182
|
matchBase: true
|
|
293
183
|
});
|
|
294
184
|
if (matchesPositive && !matchesNegative) {
|
|
@@ -308,6 +198,73 @@ const CODE_BLOCK_TYPES = [
|
|
|
308
198
|
*/ function extractGlobsFromIncludes(includes) {
|
|
309
199
|
return includes.map((p)=>parseIncludePattern(p).glob).filter((g)=>g.length > 0);
|
|
310
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* 检查单个文件是否匹配 includes 模式列表,支持 `status|glob` 前缀语法。
|
|
203
|
+
*
|
|
204
|
+
* - 当 `fileStatus` 未提供时,status 前缀的 includes 降级为纯 glob 匹配(向后兼容)
|
|
205
|
+
* - 当 `fileStatus` 提供时,完整支持 `added|`/`modified|`/`deleted|` 前缀语义
|
|
206
|
+
*
|
|
207
|
+
* 算法与 `filterFilesByIncludes` 一致,但针对单文件场景优化:
|
|
208
|
+
* 1. 排除模式(`!`) 优先过滤
|
|
209
|
+
* 2. 无前缀正向 glob 匹配
|
|
210
|
+
* 3. 有 status 前缀的 glob 按文件实际 status 过滤
|
|
211
|
+
*
|
|
212
|
+
* @param includes include 模式列表
|
|
213
|
+
* @param filename 待匹配的文件名
|
|
214
|
+
* @param fileStatus 文件变更状态(如 "added"/"modified"/"removed"),不提供时降级为纯 glob
|
|
215
|
+
* @returns 是否匹配
|
|
216
|
+
*/ function matchIncludes(includes, filename, fileStatus) {
|
|
217
|
+
if (!includes || includes.length === 0) return true;
|
|
218
|
+
if (!filename) return false;
|
|
219
|
+
const parsed = includes.map(parseIncludePattern);
|
|
220
|
+
// 无 status 信息时降级为纯 glob 匹配(向后兼容)
|
|
221
|
+
if (!fileStatus) {
|
|
222
|
+
const globs = extractGlobsFromIncludes(includes);
|
|
223
|
+
if (globs.length === 0) return true;
|
|
224
|
+
return micromatch.isMatch(filename, globs, {
|
|
225
|
+
matchBase: true
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const normalizedStatus = STATUS_ALIAS[fileStatus.toLowerCase()] ?? "modified";
|
|
229
|
+
// 排除模式(以 ! 开头),用于最终全局过滤
|
|
230
|
+
const negativeGlobs = parsed.filter((p)=>p.status === undefined && p.glob.startsWith("!")).map((p)=>p.glob.slice(1));
|
|
231
|
+
// 无前缀的正向 globs
|
|
232
|
+
const plainGlobs = parsed.filter((p)=>p.status === undefined && !p.glob.startsWith("!")).map((p)=>p.glob);
|
|
233
|
+
// 有 status 前缀的 patterns
|
|
234
|
+
const statusPatterns = parsed.filter((p)=>p.status !== undefined);
|
|
235
|
+
// 最终排除:命中排除模式的文件直接过滤掉
|
|
236
|
+
if (negativeGlobs.length > 0 && micromatch.isMatch(filename, negativeGlobs, {
|
|
237
|
+
matchBase: true
|
|
238
|
+
})) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
// 正向匹配:无前缀 glob
|
|
242
|
+
if (plainGlobs.length > 0 && micromatch.isMatch(filename, plainGlobs, {
|
|
243
|
+
matchBase: true
|
|
244
|
+
})) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
// 正向匹配:有 status 前缀的 glob,按文件实际 status 过滤
|
|
248
|
+
if (statusPatterns.length > 0) {
|
|
249
|
+
const matchingStatusGlobs = statusPatterns.filter(({ status })=>status === normalizedStatus).map(({ glob })=>glob);
|
|
250
|
+
if (matchingStatusGlobs.length > 0) {
|
|
251
|
+
const positiveGlobs = matchingStatusGlobs.filter((g)=>!g.startsWith("!"));
|
|
252
|
+
const negativeStatusGlobs = matchingStatusGlobs.filter((g)=>g.startsWith("!")).map((g)=>g.slice(1));
|
|
253
|
+
if (positiveGlobs.length > 0) {
|
|
254
|
+
const matchesPositive = micromatch.isMatch(filename, positiveGlobs, {
|
|
255
|
+
matchBase: true
|
|
256
|
+
});
|
|
257
|
+
const matchesNegative = negativeStatusGlobs.length > 0 && micromatch.isMatch(filename, negativeStatusGlobs, {
|
|
258
|
+
matchBase: true
|
|
259
|
+
});
|
|
260
|
+
if (matchesPositive && !matchesNegative) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
311
268
|
/**
|
|
312
269
|
* 从 whenModifiedCode 配置中解析代码结构过滤类型。
|
|
313
270
|
* 只接受简单的类型名称,如 "function"、"class"、"interface"、"type"、"method"
|
|
@@ -324,6 +281,44 @@ const CODE_BLOCK_TYPES = [
|
|
|
324
281
|
];
|
|
325
282
|
}
|
|
326
283
|
|
|
284
|
+
;// CONCATENATED MODULE: ./src/prompt/specs-section.ts
|
|
285
|
+
/**
|
|
286
|
+
* 构建 specs 的 prompt 部分
|
|
287
|
+
*/ function buildSpecsSection(specs) {
|
|
288
|
+
return specs.map((spec)=>{
|
|
289
|
+
const firstRule = spec.rules[0];
|
|
290
|
+
const rulesText = spec.rules.slice(1).map((rule)=>{
|
|
291
|
+
let text = `#### [${rule.id}] ${rule.title}\n`;
|
|
292
|
+
if (rule.description) {
|
|
293
|
+
text += `${rule.description}\n`;
|
|
294
|
+
}
|
|
295
|
+
if (rule.examples.length > 0) {
|
|
296
|
+
for (const example of rule.examples){
|
|
297
|
+
text += formatExample(example);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return text;
|
|
301
|
+
}).join("\n");
|
|
302
|
+
return `### ${firstRule.title}\n- 规范文件: ${spec.filename}\n- 适用扩展名: ${spec.extensions.join(", ")}\n\n${rulesText}`;
|
|
303
|
+
}).join("\n\n-------------------\n\n");
|
|
304
|
+
}
|
|
305
|
+
function formatExample(example) {
|
|
306
|
+
let text = "";
|
|
307
|
+
if (example.title) {
|
|
308
|
+
text += `##### Example: ${example.title}\n`;
|
|
309
|
+
}
|
|
310
|
+
if (example.description) {
|
|
311
|
+
text += `${example.description}\n`;
|
|
312
|
+
}
|
|
313
|
+
for (const item of example.content){
|
|
314
|
+
text += formatContent(item);
|
|
315
|
+
}
|
|
316
|
+
return text;
|
|
317
|
+
}
|
|
318
|
+
function formatContent(item) {
|
|
319
|
+
return `###### ${item.type}${item.title ? `: ${item.title}` : ""}\n${item.description}\n`;
|
|
320
|
+
}
|
|
321
|
+
|
|
327
322
|
;// CONCATENATED MODULE: ./src/review-spec/review-spec.service.ts
|
|
328
323
|
|
|
329
324
|
|
|
@@ -838,7 +833,7 @@ class ReviewSpecService {
|
|
|
838
833
|
const overrides = this.extractOverrides(ruleContent);
|
|
839
834
|
// 提取描述:在第一个例子之前的文本
|
|
840
835
|
let description = ruleContent;
|
|
841
|
-
const firstExampleIndex = ruleContent.search(/(?:^|\n)
|
|
836
|
+
const firstExampleIndex = ruleContent.search(/(?:^|\n)(?:####\s+(?:good|bad)|###\s+Example:)/i);
|
|
842
837
|
if (firstExampleIndex !== -1) {
|
|
843
838
|
description = ruleContent.slice(0, firstExampleIndex).trim();
|
|
844
839
|
} else {
|
|
@@ -903,26 +898,58 @@ class ReviewSpecService {
|
|
|
903
898
|
}
|
|
904
899
|
extractExamples(content) {
|
|
905
900
|
const examples = [];
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
901
|
+
// 按 ### Example: 分组
|
|
902
|
+
const groupSections = content.split(/(?:^|\n)(?=###\s+Example:)/);
|
|
903
|
+
for (const groupSection of groupSections){
|
|
904
|
+
const trimmedGroup = groupSection.trim();
|
|
905
|
+
if (!trimmedGroup) continue;
|
|
906
|
+
// 提取分组标题和描述,如 "### Example: 函数行数"
|
|
907
|
+
let exampleTitle = "";
|
|
908
|
+
let exampleDescription = "";
|
|
909
|
+
const groupMatch = trimmedGroup.match(/^###\s+Example\s*[::]\s*(.+)/i);
|
|
910
|
+
if (groupMatch) {
|
|
911
|
+
exampleTitle = groupMatch[1].trim();
|
|
912
|
+
// 提取标题行和第一个 #### 之间的文本作为 description
|
|
913
|
+
const afterTitle = trimmedGroup.slice(trimmedGroup.indexOf("\n")).trim();
|
|
914
|
+
const firstSubIdx = afterTitle.search(/(?:^|\n)####\s+/);
|
|
915
|
+
if (firstSubIdx > 0) {
|
|
916
|
+
exampleDescription = afterTitle.slice(0, firstSubIdx).trim();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// 在分组内按 #### 提取 Good/Bad
|
|
920
|
+
const ruleContents = [];
|
|
921
|
+
const sections = trimmedGroup.split(/(?:^|\n)####\s+/);
|
|
922
|
+
for (const section of sections){
|
|
923
|
+
const trimmedSection = section.trim();
|
|
924
|
+
if (!trimmedSection) continue;
|
|
925
|
+
let type = null;
|
|
926
|
+
let contentTitle = "";
|
|
927
|
+
if (/^good:/i.test(trimmedSection)) {
|
|
928
|
+
type = "good";
|
|
929
|
+
contentTitle = trimmedSection.match(/^good:\s*(.+)/i)?.[1]?.trim() ?? "";
|
|
930
|
+
} else if (/^bad:/i.test(trimmedSection)) {
|
|
931
|
+
type = "bad";
|
|
932
|
+
contentTitle = trimmedSection.match(/^bad:\s*(.+)/i)?.[1]?.trim() ?? "";
|
|
933
|
+
}
|
|
934
|
+
if (!type) continue;
|
|
935
|
+
// 提取代码块作为 description
|
|
936
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
937
|
+
let codeMatch;
|
|
938
|
+
const codeParts = [];
|
|
939
|
+
while((codeMatch = codeBlockRegex.exec(trimmedSection)) !== null){
|
|
940
|
+
codeParts.push(codeMatch[2].trim());
|
|
941
|
+
}
|
|
942
|
+
ruleContents.push({
|
|
943
|
+
title: contentTitle,
|
|
944
|
+
type,
|
|
945
|
+
description: codeParts.join("\n\n")
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (ruleContents.length > 0) {
|
|
922
949
|
examples.push({
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
950
|
+
title: exampleTitle,
|
|
951
|
+
description: exampleDescription,
|
|
952
|
+
content: ruleContents
|
|
926
953
|
});
|
|
927
954
|
}
|
|
928
955
|
}
|
|
@@ -959,7 +986,8 @@ class ReviewSpecService {
|
|
|
959
986
|
* 根据 spec 的 includes 配置过滤 issues
|
|
960
987
|
* 只保留文件名匹配对应 spec includes 模式的 issues
|
|
961
988
|
* 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
|
|
962
|
-
|
|
989
|
+
* 支持 `added|`/`modified|`/`deleted|` 前缀语法
|
|
990
|
+
*/ filterIssuesByIncludes(issues, specs, changedFiles) {
|
|
963
991
|
// 构建 spec filename -> includes 的映射
|
|
964
992
|
const specIncludesMap = new Map();
|
|
965
993
|
for (const spec of specs){
|
|
@@ -975,16 +1003,9 @@ class ReviewSpecService {
|
|
|
975
1003
|
if (includes.length === 0) {
|
|
976
1004
|
return true;
|
|
977
1005
|
}
|
|
978
|
-
// 检查文件是否匹配 includes
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
const matches = micromatch_0.isMatch(issue.file, globs, {
|
|
982
|
-
matchBase: true
|
|
983
|
-
});
|
|
984
|
-
if (!matches) {
|
|
985
|
-
// console.log(` Issue [${issue.ruleId}] 在文件 ${issue.file} 不匹配 includes 模式,跳过`);
|
|
986
|
-
}
|
|
987
|
-
return matches;
|
|
1006
|
+
// 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
|
|
1007
|
+
const fileStatus = changedFiles?.getStatus(issue.file);
|
|
1008
|
+
return matchIncludes(includes, issue.file, fileStatus);
|
|
988
1009
|
});
|
|
989
1010
|
}
|
|
990
1011
|
/**
|
|
@@ -1017,7 +1038,7 @@ class ReviewSpecService {
|
|
|
1017
1038
|
* @param specs - 已加载的 ReviewSpec 列表
|
|
1018
1039
|
* @param verbose - 日志详细级别:1=基础统计,3=详细收集过程
|
|
1019
1040
|
* @returns 过滤后的 issues 列表(排除了被 override 的规则产生的 issues)
|
|
1020
|
-
*/ filterIssuesByOverrides(issues, specs, verbose) {
|
|
1041
|
+
*/ filterIssuesByOverrides(issues, specs, changedFiles, verbose) {
|
|
1021
1042
|
// ========== 阶段1: 收集 spec -> overrides 的映射(保留作用域信息) ==========
|
|
1022
1043
|
// 每个 override 需要记录其来源 spec 的 includes,用于作用域判断
|
|
1023
1044
|
const scopedOverrides = [];
|
|
@@ -1073,12 +1094,9 @@ class ReviewSpecService {
|
|
|
1073
1094
|
if (scoped.includes.length === 0) {
|
|
1074
1095
|
return true;
|
|
1075
1096
|
}
|
|
1076
|
-
// 使用
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
return issueFile && micromatch_0.isMatch(issueFile, globs, {
|
|
1080
|
-
matchBase: true
|
|
1081
|
-
});
|
|
1097
|
+
// 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
|
|
1098
|
+
const fileStatus = changedFiles?.getStatus(issueFile);
|
|
1099
|
+
return matchIncludes(scoped.includes, issueFile, fileStatus);
|
|
1082
1100
|
});
|
|
1083
1101
|
if (matched) {
|
|
1084
1102
|
skipped.push({
|
|
@@ -1177,23 +1195,7 @@ class ReviewSpecService {
|
|
|
1177
1195
|
/**
|
|
1178
1196
|
* 构建 specs 的 prompt 部分
|
|
1179
1197
|
*/ buildSpecsSection(specs) {
|
|
1180
|
-
return specs
|
|
1181
|
-
const firstRule = spec.rules[0];
|
|
1182
|
-
const rulesText = spec.rules.slice(1).map((rule)=>{
|
|
1183
|
-
let text = `#### [${rule.id}] ${rule.title}\n`;
|
|
1184
|
-
if (rule.description) {
|
|
1185
|
-
text += `${rule.description}\n`;
|
|
1186
|
-
}
|
|
1187
|
-
if (rule.examples.length > 0) {
|
|
1188
|
-
for (const example of rule.examples){
|
|
1189
|
-
text += `##### ${example.type === "good" ? "推荐做法 (Good)" : "不推荐做法 (Bad)"}\n`;
|
|
1190
|
-
text += `\`\`\`${example.lang}\n${example.code}\n\`\`\`\n`;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
return text;
|
|
1194
|
-
}).join("\n");
|
|
1195
|
-
return `### ${firstRule.title}\n- 规范文件: ${spec.filename}\n- 适用扩展名: ${spec.extensions.join(", ")}\n\n${rulesText}`;
|
|
1196
|
-
}).join("\n\n-------------------\n\n");
|
|
1198
|
+
return buildSpecsSection(specs);
|
|
1197
1199
|
}
|
|
1198
1200
|
/**
|
|
1199
1201
|
* 根据 ruleId 查找规则定义
|
|
@@ -3263,6 +3265,15 @@ function generateIssueKey(issue) {
|
|
|
3263
3265
|
map(fn) {
|
|
3264
3266
|
return this._files.map(fn);
|
|
3265
3267
|
}
|
|
3268
|
+
/**
|
|
3269
|
+
* 获取指定文件的变更状态
|
|
3270
|
+
*/ getStatus(filename) {
|
|
3271
|
+
if (!filename) return undefined;
|
|
3272
|
+
for (const f of this._files){
|
|
3273
|
+
if (f.filename === filename) return f.status;
|
|
3274
|
+
}
|
|
3275
|
+
return undefined;
|
|
3276
|
+
}
|
|
3266
3277
|
countByStatus() {
|
|
3267
3278
|
let added = 0, modified = 0, deleted = 0;
|
|
3268
3279
|
for (const f of this._files){
|
|
@@ -4753,7 +4764,6 @@ function checkMaxLinesPerFile(changedFiles, fileContents, rule, round, verbose)
|
|
|
4753
4764
|
|
|
4754
4765
|
|
|
4755
4766
|
|
|
4756
|
-
|
|
4757
4767
|
class ReviewLlmProcessor {
|
|
4758
4768
|
llmProxyService;
|
|
4759
4769
|
reviewSpecService;
|
|
@@ -4786,7 +4796,8 @@ class ReviewLlmProcessor {
|
|
|
4786
4796
|
* 根据文件过滤 specs,只返回与该文件匹配的规则
|
|
4787
4797
|
* - 如果 spec 有 includes 配置,只有当文件名匹配 includes 模式时才包含该 spec
|
|
4788
4798
|
* - 如果 spec 没有 includes 配置,则按扩展名匹配
|
|
4789
|
-
|
|
4799
|
+
* - 支持 `added|`/`modified|`/`deleted|` 前缀语法,需传入 fileStatus
|
|
4800
|
+
*/ filterSpecsForFile(specs, filename, fileStatus) {
|
|
4790
4801
|
const ext = extname(filename).slice(1).toLowerCase();
|
|
4791
4802
|
if (!ext) return [];
|
|
4792
4803
|
return specs.filter((spec)=>{
|
|
@@ -4795,13 +4806,9 @@ class ReviewLlmProcessor {
|
|
|
4795
4806
|
return false;
|
|
4796
4807
|
}
|
|
4797
4808
|
// 如果有 includes 配置,检查文件名是否匹配 includes 模式
|
|
4798
|
-
//
|
|
4809
|
+
// 使用 matchIncludes 支持 status|glob 前缀语法
|
|
4799
4810
|
if (spec.includes.length > 0) {
|
|
4800
|
-
|
|
4801
|
-
if (globs.length === 0) return true;
|
|
4802
|
-
return micromatch_0.isMatch(filename, globs, {
|
|
4803
|
-
matchBase: true
|
|
4804
|
-
});
|
|
4811
|
+
return matchIncludes(spec.includes, filename, fileStatus);
|
|
4805
4812
|
}
|
|
4806
4813
|
// 没有 includes 配置,扩展名匹配即可
|
|
4807
4814
|
return true;
|
|
@@ -4833,7 +4840,7 @@ class ReviewLlmProcessor {
|
|
|
4833
4840
|
const filePrompts = await Promise.all(fileDataList.filter((item)=>item !== null).map(async ({ filename, file, contentLines, commitsSection })=>{
|
|
4834
4841
|
const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
|
|
4835
4842
|
// 根据文件过滤 specs,只注入与当前文件匹配的规则
|
|
4836
|
-
const fileSpecs = this.filterSpecsForFile(specs, filename);
|
|
4843
|
+
const fileSpecs = this.filterSpecsForFile(specs, filename, file.status);
|
|
4837
4844
|
// 从全局 whenModifiedCode 配置中解析代码结构过滤类型
|
|
4838
4845
|
const codeBlockTypes = whenModifiedCode ? extractCodeBlockTypes(whenModifiedCode) : [];
|
|
4839
4846
|
// 构建带行号的内容:有 code-* 过滤时只输出匹配的代码块范围
|
|
@@ -5504,7 +5511,7 @@ class ReviewLlmProcessor {
|
|
|
5504
5511
|
for (const commit of commits){
|
|
5505
5512
|
if (!commit.sha) continue;
|
|
5506
5513
|
const commitFiles = await this.issueFilter.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
5507
|
-
if (
|
|
5514
|
+
if (micromatch.some(commitFiles, globs)) {
|
|
5508
5515
|
filteredCommits.push(commit);
|
|
5509
5516
|
}
|
|
5510
5517
|
}
|
|
@@ -5878,7 +5885,7 @@ class ReviewService {
|
|
|
5878
5885
|
*/ filterNewIssues(issues, specs, opts) {
|
|
5879
5886
|
const { commits, fileContents, changedFiles, isDirectFileMode, context } = opts;
|
|
5880
5887
|
const { verbose } = context;
|
|
5881
|
-
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs);
|
|
5888
|
+
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs, changedFiles);
|
|
5882
5889
|
if (shouldLog(verbose, 1)) {
|
|
5883
5890
|
console.log(` 应用 includes 过滤后: ${filtered.length} 个问题`);
|
|
5884
5891
|
}
|
|
@@ -5886,7 +5893,7 @@ class ReviewService {
|
|
|
5886
5893
|
if (shouldLog(verbose, 1)) {
|
|
5887
5894
|
console.log(` 应用规则存在性过滤后: ${filtered.length} 个问题`);
|
|
5888
5895
|
}
|
|
5889
|
-
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, verbose);
|
|
5896
|
+
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, changedFiles, verbose);
|
|
5890
5897
|
// 变更行过滤
|
|
5891
5898
|
if (shouldLog(verbose, 3)) {
|
|
5892
5899
|
console.log(` 🔍 变更行过滤条件检查:`);
|
|
@@ -6478,7 +6485,7 @@ class DeletionImpactService {
|
|
|
6478
6485
|
const beforeCount = deletedBlocks.length;
|
|
6479
6486
|
const globs = extractGlobsFromIncludes(context.includes);
|
|
6480
6487
|
const filenames = deletedBlocks.map((b)=>b.file);
|
|
6481
|
-
const matchedFilenames =
|
|
6488
|
+
const matchedFilenames = micromatch(filenames, globs);
|
|
6482
6489
|
deletedBlocks = deletedBlocks.filter((b)=>matchedFilenames.includes(b.file));
|
|
6483
6490
|
if (shouldLog(verbose, 1)) {
|
|
6484
6491
|
console.log(` 🔍 Includes 过滤: ${beforeCount} -> ${deletedBlocks.length} 个删除块`);
|
|
@@ -7167,16 +7174,9 @@ const tools = [
|
|
|
7167
7174
|
filename: filePath
|
|
7168
7175
|
}
|
|
7169
7176
|
]));
|
|
7170
|
-
const micromatchModule = await __webpack_require__.e(/* import() */ "551").then(__webpack_require__.bind(__webpack_require__, 946));
|
|
7171
|
-
const micromatch = micromatchModule.default || micromatchModule;
|
|
7172
7177
|
const rules = applicableSpecs.flatMap((spec)=>spec.rules.filter((rule)=>{
|
|
7173
7178
|
const includes = rule.includes || spec.includes;
|
|
7174
|
-
|
|
7175
|
-
const globs = extractGlobsFromIncludes(includes);
|
|
7176
|
-
if (globs.length === 0) return true;
|
|
7177
|
-
return micromatch.isMatch(filePath, globs, {
|
|
7178
|
-
matchBase: true
|
|
7179
|
-
});
|
|
7179
|
+
return matchIncludes(includes, filePath);
|
|
7180
7180
|
}).map((rule)=>({
|
|
7181
7181
|
id: rule.id,
|
|
7182
7182
|
title: rule.title,
|
|
@@ -7185,9 +7185,13 @@ const tools = [
|
|
|
7185
7185
|
specFile: spec.filename,
|
|
7186
7186
|
...includeExamples && rule.examples.length > 0 ? {
|
|
7187
7187
|
examples: rule.examples.map((ex)=>({
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7188
|
+
title: ex.title,
|
|
7189
|
+
description: ex.description,
|
|
7190
|
+
content: ex.content.map((c)=>({
|
|
7191
|
+
title: c.title,
|
|
7192
|
+
type: c.type,
|
|
7193
|
+
description: c.description
|
|
7194
|
+
}))
|
|
7191
7195
|
}))
|
|
7192
7196
|
} : {}
|
|
7193
7197
|
})));
|
|
@@ -7226,9 +7230,13 @@ const tools = [
|
|
|
7226
7230
|
includes: spec.includes,
|
|
7227
7231
|
overrides: rule.overrides,
|
|
7228
7232
|
examples: rule.examples.map((ex)=>({
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7233
|
+
title: ex.title,
|
|
7234
|
+
description: ex.description,
|
|
7235
|
+
content: ex.content.map((c)=>({
|
|
7236
|
+
title: c.title,
|
|
7237
|
+
type: c.type,
|
|
7238
|
+
description: c.description
|
|
7239
|
+
}))
|
|
7232
7240
|
}))
|
|
7233
7241
|
};
|
|
7234
7242
|
}
|
|
@@ -7260,9 +7268,13 @@ const tools = [
|
|
|
7260
7268
|
includes: spec.includes,
|
|
7261
7269
|
...includeExamples && rule.examples.length > 0 ? {
|
|
7262
7270
|
examples: rule.examples.map((ex)=>({
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7271
|
+
title: ex.title,
|
|
7272
|
+
description: ex.description,
|
|
7273
|
+
content: ex.content.map((c)=>({
|
|
7274
|
+
title: c.title,
|
|
7275
|
+
type: c.type,
|
|
7276
|
+
description: c.description
|
|
7277
|
+
}))
|
|
7266
7278
|
}))
|
|
7267
7279
|
} : {
|
|
7268
7280
|
hasExamples: rule.examples.length > 0
|
package/package.json
CHANGED
|
@@ -59,6 +59,17 @@ export class ChangedFileCollection implements Iterable<ChangedFile> {
|
|
|
59
59
|
return this._files.map(fn);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 获取指定文件的变更状态
|
|
64
|
+
*/
|
|
65
|
+
getStatus(filename: string): string | undefined {
|
|
66
|
+
if (!filename) return undefined;
|
|
67
|
+
for (const f of this._files) {
|
|
68
|
+
if (f.filename === filename) return f.status;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
countByStatus(): FileStatusCount {
|
|
63
74
|
let added = 0,
|
|
64
75
|
modified = 0,
|