@spaceflow/review 0.83.0 → 1.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 +12 -0
- package/dist/index.js +209 -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 +229 -11
- package/src/review-spec/review-spec.service.ts +69 -55
- 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,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.83.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.82.0...@spaceflow/review@0.83.0) (2026-04-10)
|
|
4
|
+
|
|
5
|
+
### 代码重构
|
|
6
|
+
|
|
7
|
+
* **review:** 在 showAll=false 时过滤 merge commit 并屏蔽非本次 PR 的变更行 hash ([4edd6e2](https://github.com/Lydanne/spaceflow/commit/4edd6e285085c5fe22238eff273e7a3a683e4dc0))
|
|
8
|
+
|
|
9
|
+
### 其他修改
|
|
10
|
+
|
|
11
|
+
* **review-summary:** released version 0.51.0 [no ci] ([a76f9a4](https://github.com/Lydanne/spaceflow/commit/a76f9a4e0de05f209318fa3527532c5a54ada2bc))
|
|
12
|
+
* **scripts:** released version 0.34.0 [no ci] ([9bb3509](https://github.com/Lydanne/spaceflow/commit/9bb35096a2b1c42d6dd9b1836128249c622d22d6))
|
|
13
|
+
* **shell:** released version 0.34.0 [no ci] ([6a1638f](https://github.com/Lydanne/spaceflow/commit/6a1638fe69b0f086ddde4075bee2a76604f27ccb))
|
|
14
|
+
|
|
3
15
|
## [0.82.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.81.0...@spaceflow/review@0.82.0) (2026-04-09)
|
|
4
16
|
|
|
5
17
|
### 代码重构
|
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.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,53 @@ 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 = "Example";
|
|
912
|
+
exampleDescription = groupMatch[1].trim();
|
|
913
|
+
}
|
|
914
|
+
// 在分组内按 #### 提取 Good/Bad
|
|
915
|
+
const ruleContents = [];
|
|
916
|
+
const sections = trimmedGroup.split(/(?:^|\n)####\s+/);
|
|
917
|
+
for (const section of sections){
|
|
918
|
+
const trimmedSection = section.trim();
|
|
919
|
+
if (!trimmedSection) continue;
|
|
920
|
+
let type = null;
|
|
921
|
+
let contentTitle = "";
|
|
922
|
+
if (/^good:/i.test(trimmedSection)) {
|
|
923
|
+
type = "good";
|
|
924
|
+
contentTitle = trimmedSection.match(/^good:\s*(.+)/i)?.[1]?.trim() ?? "";
|
|
925
|
+
} else if (/^bad:/i.test(trimmedSection)) {
|
|
926
|
+
type = "bad";
|
|
927
|
+
contentTitle = trimmedSection.match(/^bad:\s*(.+)/i)?.[1]?.trim() ?? "";
|
|
928
|
+
}
|
|
929
|
+
if (!type) continue;
|
|
930
|
+
// 提取代码块作为 description
|
|
931
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
932
|
+
let codeMatch;
|
|
933
|
+
const codeParts = [];
|
|
934
|
+
while((codeMatch = codeBlockRegex.exec(trimmedSection)) !== null){
|
|
935
|
+
codeParts.push(codeMatch[2].trim());
|
|
936
|
+
}
|
|
937
|
+
ruleContents.push({
|
|
938
|
+
title: contentTitle,
|
|
939
|
+
type,
|
|
940
|
+
description: codeParts.join("\n\n")
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (ruleContents.length > 0) {
|
|
922
944
|
examples.push({
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
945
|
+
title: exampleTitle,
|
|
946
|
+
description: exampleDescription,
|
|
947
|
+
content: ruleContents
|
|
926
948
|
});
|
|
927
949
|
}
|
|
928
950
|
}
|
|
@@ -959,7 +981,8 @@ class ReviewSpecService {
|
|
|
959
981
|
* 根据 spec 的 includes 配置过滤 issues
|
|
960
982
|
* 只保留文件名匹配对应 spec includes 模式的 issues
|
|
961
983
|
* 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
|
|
962
|
-
|
|
984
|
+
* 支持 `added|`/`modified|`/`deleted|` 前缀语法
|
|
985
|
+
*/ filterIssuesByIncludes(issues, specs, changedFiles) {
|
|
963
986
|
// 构建 spec filename -> includes 的映射
|
|
964
987
|
const specIncludesMap = new Map();
|
|
965
988
|
for (const spec of specs){
|
|
@@ -975,16 +998,9 @@ class ReviewSpecService {
|
|
|
975
998
|
if (includes.length === 0) {
|
|
976
999
|
return true;
|
|
977
1000
|
}
|
|
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;
|
|
1001
|
+
// 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
|
|
1002
|
+
const fileStatus = changedFiles?.getStatus(issue.file);
|
|
1003
|
+
return matchIncludes(includes, issue.file, fileStatus);
|
|
988
1004
|
});
|
|
989
1005
|
}
|
|
990
1006
|
/**
|
|
@@ -1017,7 +1033,7 @@ class ReviewSpecService {
|
|
|
1017
1033
|
* @param specs - 已加载的 ReviewSpec 列表
|
|
1018
1034
|
* @param verbose - 日志详细级别:1=基础统计,3=详细收集过程
|
|
1019
1035
|
* @returns 过滤后的 issues 列表(排除了被 override 的规则产生的 issues)
|
|
1020
|
-
*/ filterIssuesByOverrides(issues, specs, verbose) {
|
|
1036
|
+
*/ filterIssuesByOverrides(issues, specs, changedFiles, verbose) {
|
|
1021
1037
|
// ========== 阶段1: 收集 spec -> overrides 的映射(保留作用域信息) ==========
|
|
1022
1038
|
// 每个 override 需要记录其来源 spec 的 includes,用于作用域判断
|
|
1023
1039
|
const scopedOverrides = [];
|
|
@@ -1073,12 +1089,9 @@ class ReviewSpecService {
|
|
|
1073
1089
|
if (scoped.includes.length === 0) {
|
|
1074
1090
|
return true;
|
|
1075
1091
|
}
|
|
1076
|
-
// 使用
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
return issueFile && micromatch_0.isMatch(issueFile, globs, {
|
|
1080
|
-
matchBase: true
|
|
1081
|
-
});
|
|
1092
|
+
// 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
|
|
1093
|
+
const fileStatus = changedFiles?.getStatus(issueFile);
|
|
1094
|
+
return matchIncludes(scoped.includes, issueFile, fileStatus);
|
|
1082
1095
|
});
|
|
1083
1096
|
if (matched) {
|
|
1084
1097
|
skipped.push({
|
|
@@ -1177,23 +1190,7 @@ class ReviewSpecService {
|
|
|
1177
1190
|
/**
|
|
1178
1191
|
* 构建 specs 的 prompt 部分
|
|
1179
1192
|
*/ 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");
|
|
1193
|
+
return buildSpecsSection(specs);
|
|
1197
1194
|
}
|
|
1198
1195
|
/**
|
|
1199
1196
|
* 根据 ruleId 查找规则定义
|
|
@@ -3263,6 +3260,15 @@ function generateIssueKey(issue) {
|
|
|
3263
3260
|
map(fn) {
|
|
3264
3261
|
return this._files.map(fn);
|
|
3265
3262
|
}
|
|
3263
|
+
/**
|
|
3264
|
+
* 获取指定文件的变更状态
|
|
3265
|
+
*/ getStatus(filename) {
|
|
3266
|
+
if (!filename) return undefined;
|
|
3267
|
+
for (const f of this._files){
|
|
3268
|
+
if (f.filename === filename) return f.status;
|
|
3269
|
+
}
|
|
3270
|
+
return undefined;
|
|
3271
|
+
}
|
|
3266
3272
|
countByStatus() {
|
|
3267
3273
|
let added = 0, modified = 0, deleted = 0;
|
|
3268
3274
|
for (const f of this._files){
|
|
@@ -4753,7 +4759,6 @@ function checkMaxLinesPerFile(changedFiles, fileContents, rule, round, verbose)
|
|
|
4753
4759
|
|
|
4754
4760
|
|
|
4755
4761
|
|
|
4756
|
-
|
|
4757
4762
|
class ReviewLlmProcessor {
|
|
4758
4763
|
llmProxyService;
|
|
4759
4764
|
reviewSpecService;
|
|
@@ -4786,7 +4791,8 @@ class ReviewLlmProcessor {
|
|
|
4786
4791
|
* 根据文件过滤 specs,只返回与该文件匹配的规则
|
|
4787
4792
|
* - 如果 spec 有 includes 配置,只有当文件名匹配 includes 模式时才包含该 spec
|
|
4788
4793
|
* - 如果 spec 没有 includes 配置,则按扩展名匹配
|
|
4789
|
-
|
|
4794
|
+
* - 支持 `added|`/`modified|`/`deleted|` 前缀语法,需传入 fileStatus
|
|
4795
|
+
*/ filterSpecsForFile(specs, filename, fileStatus) {
|
|
4790
4796
|
const ext = extname(filename).slice(1).toLowerCase();
|
|
4791
4797
|
if (!ext) return [];
|
|
4792
4798
|
return specs.filter((spec)=>{
|
|
@@ -4795,13 +4801,9 @@ class ReviewLlmProcessor {
|
|
|
4795
4801
|
return false;
|
|
4796
4802
|
}
|
|
4797
4803
|
// 如果有 includes 配置,检查文件名是否匹配 includes 模式
|
|
4798
|
-
//
|
|
4804
|
+
// 使用 matchIncludes 支持 status|glob 前缀语法
|
|
4799
4805
|
if (spec.includes.length > 0) {
|
|
4800
|
-
|
|
4801
|
-
if (globs.length === 0) return true;
|
|
4802
|
-
return micromatch_0.isMatch(filename, globs, {
|
|
4803
|
-
matchBase: true
|
|
4804
|
-
});
|
|
4806
|
+
return matchIncludes(spec.includes, filename, fileStatus);
|
|
4805
4807
|
}
|
|
4806
4808
|
// 没有 includes 配置,扩展名匹配即可
|
|
4807
4809
|
return true;
|
|
@@ -4833,7 +4835,7 @@ class ReviewLlmProcessor {
|
|
|
4833
4835
|
const filePrompts = await Promise.all(fileDataList.filter((item)=>item !== null).map(async ({ filename, file, contentLines, commitsSection })=>{
|
|
4834
4836
|
const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
|
|
4835
4837
|
// 根据文件过滤 specs,只注入与当前文件匹配的规则
|
|
4836
|
-
const fileSpecs = this.filterSpecsForFile(specs, filename);
|
|
4838
|
+
const fileSpecs = this.filterSpecsForFile(specs, filename, file.status);
|
|
4837
4839
|
// 从全局 whenModifiedCode 配置中解析代码结构过滤类型
|
|
4838
4840
|
const codeBlockTypes = whenModifiedCode ? extractCodeBlockTypes(whenModifiedCode) : [];
|
|
4839
4841
|
// 构建带行号的内容:有 code-* 过滤时只输出匹配的代码块范围
|
|
@@ -5504,7 +5506,7 @@ class ReviewLlmProcessor {
|
|
|
5504
5506
|
for (const commit of commits){
|
|
5505
5507
|
if (!commit.sha) continue;
|
|
5506
5508
|
const commitFiles = await this.issueFilter.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
5507
|
-
if (
|
|
5509
|
+
if (micromatch.some(commitFiles, globs)) {
|
|
5508
5510
|
filteredCommits.push(commit);
|
|
5509
5511
|
}
|
|
5510
5512
|
}
|
|
@@ -5878,7 +5880,7 @@ class ReviewService {
|
|
|
5878
5880
|
*/ filterNewIssues(issues, specs, opts) {
|
|
5879
5881
|
const { commits, fileContents, changedFiles, isDirectFileMode, context } = opts;
|
|
5880
5882
|
const { verbose } = context;
|
|
5881
|
-
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs);
|
|
5883
|
+
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs, changedFiles);
|
|
5882
5884
|
if (shouldLog(verbose, 1)) {
|
|
5883
5885
|
console.log(` 应用 includes 过滤后: ${filtered.length} 个问题`);
|
|
5884
5886
|
}
|
|
@@ -5886,7 +5888,7 @@ class ReviewService {
|
|
|
5886
5888
|
if (shouldLog(verbose, 1)) {
|
|
5887
5889
|
console.log(` 应用规则存在性过滤后: ${filtered.length} 个问题`);
|
|
5888
5890
|
}
|
|
5889
|
-
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, verbose);
|
|
5891
|
+
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, changedFiles, verbose);
|
|
5890
5892
|
// 变更行过滤
|
|
5891
5893
|
if (shouldLog(verbose, 3)) {
|
|
5892
5894
|
console.log(` 🔍 变更行过滤条件检查:`);
|
|
@@ -6478,7 +6480,7 @@ class DeletionImpactService {
|
|
|
6478
6480
|
const beforeCount = deletedBlocks.length;
|
|
6479
6481
|
const globs = extractGlobsFromIncludes(context.includes);
|
|
6480
6482
|
const filenames = deletedBlocks.map((b)=>b.file);
|
|
6481
|
-
const matchedFilenames =
|
|
6483
|
+
const matchedFilenames = micromatch(filenames, globs);
|
|
6482
6484
|
deletedBlocks = deletedBlocks.filter((b)=>matchedFilenames.includes(b.file));
|
|
6483
6485
|
if (shouldLog(verbose, 1)) {
|
|
6484
6486
|
console.log(` 🔍 Includes 过滤: ${beforeCount} -> ${deletedBlocks.length} 个删除块`);
|
|
@@ -7167,16 +7169,9 @@ const tools = [
|
|
|
7167
7169
|
filename: filePath
|
|
7168
7170
|
}
|
|
7169
7171
|
]));
|
|
7170
|
-
const micromatchModule = await __webpack_require__.e(/* import() */ "551").then(__webpack_require__.bind(__webpack_require__, 946));
|
|
7171
|
-
const micromatch = micromatchModule.default || micromatchModule;
|
|
7172
7172
|
const rules = applicableSpecs.flatMap((spec)=>spec.rules.filter((rule)=>{
|
|
7173
7173
|
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
|
-
});
|
|
7174
|
+
return matchIncludes(includes, filePath);
|
|
7180
7175
|
}).map((rule)=>({
|
|
7181
7176
|
id: rule.id,
|
|
7182
7177
|
title: rule.title,
|
|
@@ -7185,9 +7180,13 @@ const tools = [
|
|
|
7185
7180
|
specFile: spec.filename,
|
|
7186
7181
|
...includeExamples && rule.examples.length > 0 ? {
|
|
7187
7182
|
examples: rule.examples.map((ex)=>({
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7183
|
+
title: ex.title,
|
|
7184
|
+
description: ex.description,
|
|
7185
|
+
content: ex.content.map((c)=>({
|
|
7186
|
+
title: c.title,
|
|
7187
|
+
type: c.type,
|
|
7188
|
+
description: c.description
|
|
7189
|
+
}))
|
|
7191
7190
|
}))
|
|
7192
7191
|
} : {}
|
|
7193
7192
|
})));
|
|
@@ -7226,9 +7225,13 @@ const tools = [
|
|
|
7226
7225
|
includes: spec.includes,
|
|
7227
7226
|
overrides: rule.overrides,
|
|
7228
7227
|
examples: rule.examples.map((ex)=>({
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7228
|
+
title: ex.title,
|
|
7229
|
+
description: ex.description,
|
|
7230
|
+
content: ex.content.map((c)=>({
|
|
7231
|
+
title: c.title,
|
|
7232
|
+
type: c.type,
|
|
7233
|
+
description: c.description
|
|
7234
|
+
}))
|
|
7232
7235
|
}))
|
|
7233
7236
|
};
|
|
7234
7237
|
}
|
|
@@ -7260,9 +7263,13 @@ const tools = [
|
|
|
7260
7263
|
includes: spec.includes,
|
|
7261
7264
|
...includeExamples && rule.examples.length > 0 ? {
|
|
7262
7265
|
examples: rule.examples.map((ex)=>({
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
+
title: ex.title,
|
|
7267
|
+
description: ex.description,
|
|
7268
|
+
content: ex.content.map((c)=>({
|
|
7269
|
+
title: c.title,
|
|
7270
|
+
type: c.type,
|
|
7271
|
+
description: c.description
|
|
7272
|
+
}))
|
|
7266
7273
|
}))
|
|
7267
7274
|
} : {
|
|
7268
7275
|
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,
|