@spaceflow/review 0.82.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 +45 -0
- package/dist/index.js +231 -210
- 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-issue-filter.spec.ts +52 -0
- package/src/review-llm.ts +6 -8
- package/src/review-source-resolver.ts +22 -4
- 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.spec.ts +44 -0
- package/src/review.service.ts +15 -4
- package/dist/551.js +0 -9
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-* 过滤时只输出匹配的代码块范围
|
|
@@ -5293,7 +5295,7 @@ class ReviewLlmProcessor {
|
|
|
5293
5295
|
({ commits, changedFiles } = await this.applyPreFilters(context, commits, changedFiles, isDirectFileMode));
|
|
5294
5296
|
const headSha = prModel ? await prModel.getHeadSha() : context.headRef || "HEAD";
|
|
5295
5297
|
const collectedFiles = ChangedFileCollection.from(changedFiles);
|
|
5296
|
-
const fileContents = await this.getFileContents(context.owner, context.repo, collectedFiles.toArray(), commits, headSha, context.prNumber, isLocalMode, context.verbose);
|
|
5298
|
+
const fileContents = await this.getFileContents(context.owner, context.repo, collectedFiles.toArray(), commits, headSha, context.prNumber, isLocalMode, context.showAll, context.verbose);
|
|
5297
5299
|
return {
|
|
5298
5300
|
prModel,
|
|
5299
5301
|
commits,
|
|
@@ -5435,18 +5437,20 @@ class ReviewLlmProcessor {
|
|
|
5435
5437
|
* 2. --commits — 仅保留用户指定的 commit 及其涉及的文件
|
|
5436
5438
|
* 3. --includes — glob 模式过滤文件和 commits(支持 status| 前缀语法)
|
|
5437
5439
|
*/ async applyPreFilters(context, commits, rawChangedFiles, isDirectFileMode) {
|
|
5438
|
-
const { owner, repo, prNumber, verbose, includes, files, commits: filterCommits } = context;
|
|
5440
|
+
const { owner, repo, prNumber, verbose, includes, files, commits: filterCommits, showAll } = context;
|
|
5439
5441
|
let changedFiles = ChangedFileCollection.from(rawChangedFiles);
|
|
5440
|
-
// 0. 过滤掉 merge commit
|
|
5441
|
-
{
|
|
5442
|
+
// 0. 过滤掉 merge commit(showAll=false 时启用)
|
|
5443
|
+
if (!showAll) {
|
|
5442
5444
|
const before = commits.length;
|
|
5443
5445
|
commits = commits.filter((c)=>{
|
|
5444
5446
|
const message = c.commit?.message || "";
|
|
5445
|
-
return
|
|
5447
|
+
return !/^merge\b/i.test(message);
|
|
5446
5448
|
});
|
|
5447
5449
|
if (before !== commits.length && shouldLog(verbose, 1)) {
|
|
5448
5450
|
console.log(` 跳过 Merge Commits: ${before} -> ${commits.length} 个`);
|
|
5449
5451
|
}
|
|
5452
|
+
} else if (shouldLog(verbose, 2)) {
|
|
5453
|
+
console.log(` showAll=true,跳过 Merge Commit 过滤`);
|
|
5450
5454
|
}
|
|
5451
5455
|
// 1. 按指定的 files 过滤
|
|
5452
5456
|
if (files && files.length > 0) {
|
|
@@ -5502,7 +5506,7 @@ class ReviewLlmProcessor {
|
|
|
5502
5506
|
for (const commit of commits){
|
|
5503
5507
|
if (!commit.sha) continue;
|
|
5504
5508
|
const commitFiles = await this.issueFilter.getFilesForCommit(owner, repo, commit.sha, prNumber);
|
|
5505
|
-
if (
|
|
5509
|
+
if (micromatch.some(commitFiles, globs)) {
|
|
5506
5510
|
filteredCommits.push(commit);
|
|
5507
5511
|
}
|
|
5508
5512
|
}
|
|
@@ -5520,9 +5524,11 @@ class ReviewLlmProcessor {
|
|
|
5520
5524
|
/**
|
|
5521
5525
|
* 获取文件内容并构建行号到 commit hash 的映射
|
|
5522
5526
|
* 返回 Map<filename, Array<[commitHash, lineCode]>>
|
|
5523
|
-
*/ async getFileContents(owner, repo, changedFiles, commits, ref, prNumber, isLocalMode, verbose) {
|
|
5527
|
+
*/ async getFileContents(owner, repo, changedFiles, commits, ref, prNumber, isLocalMode, showAll, verbose) {
|
|
5524
5528
|
const contents = new Map();
|
|
5525
5529
|
const latestCommitHash = commits[commits.length - 1]?.sha?.slice(0, 7) || "+local+";
|
|
5530
|
+
const validCommitHashes = new Set(commits.map((c)=>c.sha?.slice(0, 7)).filter(Boolean));
|
|
5531
|
+
const shouldMaskUnknownChangedLines = !showAll && validCommitHashes.size > 0;
|
|
5526
5532
|
if (shouldLog(verbose, 1)) {
|
|
5527
5533
|
console.log(`📊 正在构建行号到变更的映射...`);
|
|
5528
5534
|
}
|
|
@@ -5577,6 +5583,12 @@ class ReviewLlmProcessor {
|
|
|
5577
5583
|
];
|
|
5578
5584
|
}
|
|
5579
5585
|
const hash = blameMap?.get(lineNum) ?? latestCommitHash;
|
|
5586
|
+
if (shouldMaskUnknownChangedLines && !validCommitHashes.has(hash)) {
|
|
5587
|
+
return [
|
|
5588
|
+
"-------",
|
|
5589
|
+
line
|
|
5590
|
+
];
|
|
5591
|
+
}
|
|
5580
5592
|
return [
|
|
5581
5593
|
hash,
|
|
5582
5594
|
line
|
|
@@ -5868,7 +5880,7 @@ class ReviewService {
|
|
|
5868
5880
|
*/ filterNewIssues(issues, specs, opts) {
|
|
5869
5881
|
const { commits, fileContents, changedFiles, isDirectFileMode, context } = opts;
|
|
5870
5882
|
const { verbose } = context;
|
|
5871
|
-
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs);
|
|
5883
|
+
let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs, changedFiles);
|
|
5872
5884
|
if (shouldLog(verbose, 1)) {
|
|
5873
5885
|
console.log(` 应用 includes 过滤后: ${filtered.length} 个问题`);
|
|
5874
5886
|
}
|
|
@@ -5876,7 +5888,7 @@ class ReviewService {
|
|
|
5876
5888
|
if (shouldLog(verbose, 1)) {
|
|
5877
5889
|
console.log(` 应用规则存在性过滤后: ${filtered.length} 个问题`);
|
|
5878
5890
|
}
|
|
5879
|
-
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, verbose);
|
|
5891
|
+
filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, changedFiles, verbose);
|
|
5880
5892
|
// 变更行过滤
|
|
5881
5893
|
if (shouldLog(verbose, 3)) {
|
|
5882
5894
|
console.log(` 🔍 变更行过滤条件检查:`);
|
|
@@ -6014,7 +6026,11 @@ class ReviewService {
|
|
|
6014
6026
|
console.log(`📋 找到 ${resultModel.issues.length} 个历史问题`);
|
|
6015
6027
|
}
|
|
6016
6028
|
// 2. 获取 commits 并填充 author 信息
|
|
6017
|
-
const
|
|
6029
|
+
const allCommits = await prModel.getCommits();
|
|
6030
|
+
const commits = context.showAll ? allCommits : allCommits.filter((c)=>!/^merge\b/i.test(c.commit?.message || ""));
|
|
6031
|
+
if (allCommits.length !== commits.length && shouldLog(verbose, 1)) {
|
|
6032
|
+
console.log(` 跳过 Merge Commits: ${allCommits.length} -> ${commits.length} 个`);
|
|
6033
|
+
}
|
|
6018
6034
|
resultModel.issues = await this.contextBuilder.fillIssueAuthors(resultModel.issues, commits, owner, repo, verbose);
|
|
6019
6035
|
// 3. 同步已解决的评论状态
|
|
6020
6036
|
await resultModel.syncResolved();
|
|
@@ -6026,7 +6042,7 @@ class ReviewService {
|
|
|
6026
6042
|
const changedFiles = await prModel.getFiles();
|
|
6027
6043
|
const headSha = await prModel.getHeadSha();
|
|
6028
6044
|
const verifySpecs = await this.issueFilter.loadSpecs(context.specSources, verbose);
|
|
6029
|
-
const verifyFileContents = await this.sourceResolver.getFileContents(owner, repo, changedFiles, commits, headSha, prNumber, false, verbose);
|
|
6045
|
+
const verifyFileContents = await this.sourceResolver.getFileContents(owner, repo, changedFiles, commits, headSha, prNumber, false, context.showAll, verbose);
|
|
6030
6046
|
resultModel.issues = await this.issueFilter.verifyAndUpdateIssues(context, resultModel.issues, commits, {
|
|
6031
6047
|
specs: verifySpecs,
|
|
6032
6048
|
fileContents: verifyFileContents
|
|
@@ -6464,7 +6480,7 @@ class DeletionImpactService {
|
|
|
6464
6480
|
const beforeCount = deletedBlocks.length;
|
|
6465
6481
|
const globs = extractGlobsFromIncludes(context.includes);
|
|
6466
6482
|
const filenames = deletedBlocks.map((b)=>b.file);
|
|
6467
|
-
const matchedFilenames =
|
|
6483
|
+
const matchedFilenames = micromatch(filenames, globs);
|
|
6468
6484
|
deletedBlocks = deletedBlocks.filter((b)=>matchedFilenames.includes(b.file));
|
|
6469
6485
|
if (shouldLog(verbose, 1)) {
|
|
6470
6486
|
console.log(` 🔍 Includes 过滤: ${beforeCount} -> ${deletedBlocks.length} 个删除块`);
|
|
@@ -7153,16 +7169,9 @@ const tools = [
|
|
|
7153
7169
|
filename: filePath
|
|
7154
7170
|
}
|
|
7155
7171
|
]));
|
|
7156
|
-
const micromatchModule = await __webpack_require__.e(/* import() */ "551").then(__webpack_require__.bind(__webpack_require__, 946));
|
|
7157
|
-
const micromatch = micromatchModule.default || micromatchModule;
|
|
7158
7172
|
const rules = applicableSpecs.flatMap((spec)=>spec.rules.filter((rule)=>{
|
|
7159
7173
|
const includes = rule.includes || spec.includes;
|
|
7160
|
-
|
|
7161
|
-
const globs = extractGlobsFromIncludes(includes);
|
|
7162
|
-
if (globs.length === 0) return true;
|
|
7163
|
-
return micromatch.isMatch(filePath, globs, {
|
|
7164
|
-
matchBase: true
|
|
7165
|
-
});
|
|
7174
|
+
return matchIncludes(includes, filePath);
|
|
7166
7175
|
}).map((rule)=>({
|
|
7167
7176
|
id: rule.id,
|
|
7168
7177
|
title: rule.title,
|
|
@@ -7171,9 +7180,13 @@ const tools = [
|
|
|
7171
7180
|
specFile: spec.filename,
|
|
7172
7181
|
...includeExamples && rule.examples.length > 0 ? {
|
|
7173
7182
|
examples: rule.examples.map((ex)=>({
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
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
|
+
}))
|
|
7177
7190
|
}))
|
|
7178
7191
|
} : {}
|
|
7179
7192
|
})));
|
|
@@ -7212,9 +7225,13 @@ const tools = [
|
|
|
7212
7225
|
includes: spec.includes,
|
|
7213
7226
|
overrides: rule.overrides,
|
|
7214
7227
|
examples: rule.examples.map((ex)=>({
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
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
|
+
}))
|
|
7218
7235
|
}))
|
|
7219
7236
|
};
|
|
7220
7237
|
}
|
|
@@ -7246,9 +7263,13 @@ const tools = [
|
|
|
7246
7263
|
includes: spec.includes,
|
|
7247
7264
|
...includeExamples && rule.examples.length > 0 ? {
|
|
7248
7265
|
examples: rule.examples.map((ex)=>({
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
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
|
+
}))
|
|
7252
7273
|
}))
|
|
7253
7274
|
} : {
|
|
7254
7275
|
hasExamples: rule.examples.length > 0
|