@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 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 micromatch_0 from "micromatch";
6
+ import micromatch from "micromatch";
7
7
  import { existsSync } from "fs";
8
- var __webpack_modules__ = ({});
9
- // The module cache
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 && micromatch_0.isMatch(filename, negativeGlobs, {
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 && micromatch_0.isMatch(filename, plainGlobs, {
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 = micromatch_0.isMatch(filename, positiveGlobs, {
178
+ const matchesPositive = micromatch.isMatch(filename, positiveGlobs, {
289
179
  matchBase: true
290
180
  });
291
- const matchesNegative = negativeStatusGlobs.length > 0 && micromatch_0.isMatch(filename, negativeStatusGlobs, {
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)###\s+(?:good|bad)/i);
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
- const sections = content.split(/(?:^|\n)###\s+/);
907
- for (const section of sections){
908
- const trimmedSection = section.trim();
909
- if (!trimmedSection) continue;
910
- let type = null;
911
- if (/^good\b/i.test(trimmedSection)) {
912
- type = "good";
913
- } else if (/^bad\b/i.test(trimmedSection)) {
914
- type = "bad";
915
- }
916
- if (!type) continue;
917
- const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
918
- let codeMatch;
919
- while((codeMatch = codeBlockRegex.exec(trimmedSection)) !== null){
920
- const lang = codeMatch[1] || "text";
921
- const code = codeMatch[2].trim();
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
- lang,
924
- code,
925
- type
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
- */ filterIssuesByIncludes(issues, specs) {
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 模式(转换为纯 glob,避免 status| 前缀和 code-* 空串传入 micromatch)
979
- const globs = extractGlobsFromIncludes(includes);
980
- if (globs.length === 0) return true;
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
- // 使用 micromatch 检查文件是否匹配 includes 模式(转换为纯 glob
1077
- const globs = extractGlobsFromIncludes(scoped.includes);
1078
- if (globs.length === 0) return true;
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.map((spec)=>{
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
- */ filterSpecsForFile(specs, filename) {
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
- // 需先提取纯 glob(去掉 added|/modified| 前缀,过滤 code-* 空串),避免 micromatch 报错
4809
+ // 使用 matchIncludes 支持 status|glob 前缀语法
4799
4810
  if (spec.includes.length > 0) {
4800
- const globs = extractGlobsFromIncludes(spec.includes);
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 (micromatch_0.some(commitFiles, globs)) {
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 = micromatch_0(filenames, globs);
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
- if (includes.length === 0) return true;
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
- type: ex.type,
7189
- lang: ex.lang,
7190
- code: ex.code
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
- type: ex.type,
7230
- lang: ex.lang,
7231
- code: ex.code
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
- type: ex.type,
7264
- lang: ex.lang,
7265
- code: ex.code
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.83.0",
3
+ "version": "2.0.0",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -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,