@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.
@@ -35,7 +35,7 @@ describe("ReviewSpecService", () => {
35
35
  - 排除配置文件
36
36
  - 排除测试文件
37
37
 
38
- ### Good
38
+ #### Good: 合理的常量命名
39
39
  \`\`\`js
40
40
  const MAX_COUNT = 100;
41
41
  \`\`\``;
@@ -59,10 +59,13 @@ const MAX_COUNT = 100;
59
59
  expect(specs[0].rules[1].description).toContain("排除配置文件");
60
60
  expect(specs[0].rules[1].description).toContain("排除测试文件");
61
61
  expect(specs[0].rules[1].examples).toHaveLength(1);
62
- expect(specs[0].rules[1].examples[0]).toEqual({
63
- lang: "js",
64
- code: "const MAX_COUNT = 100;",
62
+ expect(specs[0].rules[1].examples[0].title).toBe("");
63
+ expect(specs[0].rules[1].examples[0].description).toBe("");
64
+ expect(specs[0].rules[1].examples[0].content).toHaveLength(1);
65
+ expect(specs[0].rules[1].examples[0].content[0]).toEqual({
66
+ title: "合理的常量命名",
65
67
  type: "good",
68
+ description: "const MAX_COUNT = 100;",
66
69
  });
67
70
 
68
71
  expect(specs[1].filename).toBe("vue.file-name.md");
@@ -309,7 +312,7 @@ const MAX_COUNT = 100;
309
312
 
310
313
  > - severity \`warn\`
311
314
 
312
- ### Good
315
+ #### Good: 合理的常量命名
313
316
  \`\`\`js
314
317
  const MAX_COUNT = 100;
315
318
  \`\`\``;
@@ -566,6 +569,92 @@ const MAX_COUNT = 100;
566
569
 
567
570
  expect(result).toHaveLength(2);
568
571
  });
572
+
573
+ it("should filter issues by spec includes with added| prefix when fileStatusMap provided", () => {
574
+ const specs = [
575
+ {
576
+ filename: "js.models.md",
577
+ extensions: ["js"],
578
+ type: "models",
579
+ content: "",
580
+ overrides: [],
581
+ severity: "error" as const,
582
+ includes: ["added|*.model.js"],
583
+ rules: [
584
+ { id: "Js.Models", title: "Models", description: "", examples: [], overrides: [] },
585
+ ],
586
+ },
587
+ ];
588
+
589
+ const issues = [
590
+ { file: "user/models/user.model.js", ruleId: "Js.Models.Rule1", reason: "test" },
591
+ { file: "user/models/product.model.js", ruleId: "Js.Models.Rule2", reason: "test" },
592
+ ];
593
+
594
+ // added 文件保留,modified 文件被 added| 前缀过滤
595
+ const changedFiles = ChangedFileCollection.from([
596
+ { filename: "user/models/user.model.js", status: "added" },
597
+ { filename: "user/models/product.model.js", status: "modified" },
598
+ ]);
599
+ const result = service.filterIssuesByIncludes(issues, specs, changedFiles);
600
+ expect(result).toHaveLength(1);
601
+ expect(result[0].file).toBe("user/models/user.model.js");
602
+ });
603
+
604
+ it("should fall back to pure glob matching when fileStatusMap not provided", () => {
605
+ const specs = [
606
+ {
607
+ filename: "js.models.md",
608
+ extensions: ["js"],
609
+ type: "models",
610
+ content: "",
611
+ overrides: [],
612
+ severity: "error" as const,
613
+ includes: ["added|*.model.js"],
614
+ rules: [
615
+ { id: "Js.Models", title: "Models", description: "", examples: [], overrides: [] },
616
+ ],
617
+ },
618
+ ];
619
+
620
+ const issues = [
621
+ { file: "user/models/user.model.js", ruleId: "Js.Models.Rule1", reason: "test" },
622
+ { file: "src/app.js", ruleId: "Js.Models.Rule2", reason: "test" },
623
+ ];
624
+
625
+ // 无 status map 时,added| 前缀降级为纯 glob 匹配
626
+ const result = service.filterIssuesByIncludes(issues, specs);
627
+ expect(result).toHaveLength(1);
628
+ expect(result[0].file).toBe("user/models/user.model.js");
629
+ });
630
+
631
+ it("should filter issues by spec includes with modified| prefix", () => {
632
+ const specs = [
633
+ {
634
+ filename: "js.nest.md",
635
+ extensions: ["ts"],
636
+ type: "nest",
637
+ content: "",
638
+ overrides: [],
639
+ severity: "error" as const,
640
+ includes: ["modified|*.controller.ts"],
641
+ rules: [{ id: "JsTs.Nest", title: "Nest", description: "", examples: [], overrides: [] }],
642
+ },
643
+ ];
644
+
645
+ const issues = [
646
+ { file: "user.controller.ts", ruleId: "JsTs.Nest.Rule1", reason: "test" },
647
+ { file: "app.controller.ts", ruleId: "JsTs.Nest.Rule2", reason: "test" },
648
+ ];
649
+
650
+ const changedFiles = ChangedFileCollection.from([
651
+ { filename: "user.controller.ts", status: "modified" },
652
+ { filename: "app.controller.ts", status: "added" },
653
+ ]);
654
+ const result = service.filterIssuesByIncludes(issues, specs, changedFiles);
655
+ expect(result).toHaveLength(1);
656
+ expect(result[0].file).toBe("user.controller.ts");
657
+ });
569
658
  });
570
659
 
571
660
  describe("matchRuleId", () => {
@@ -714,7 +803,13 @@ const MAX_COUNT = 100;
714
803
  id: "JsTs.Base.Rule1",
715
804
  title: "Rule1",
716
805
  description: "rule desc",
717
- examples: [{ lang: "ts", code: "const x = 1;", type: "good" as const }],
806
+ examples: [
807
+ {
808
+ title: "",
809
+ description: "",
810
+ content: [{ title: "", type: "good" as const, description: "const x = 1;" }],
811
+ },
812
+ ],
718
813
  overrides: [],
719
814
  },
720
815
  ],
@@ -723,7 +818,7 @@ const MAX_COUNT = 100;
723
818
  const result = service.buildSpecsSection(specs);
724
819
  expect(result).toContain("基础规范");
725
820
  expect(result).toContain("Rule1");
726
- expect(result).toContain("推荐做法");
821
+ expect(result).toContain("good");
727
822
  });
728
823
 
729
824
  it("should handle rules without examples", () => {
@@ -1163,7 +1258,7 @@ const MAX_COUNT = 100;
1163
1258
  ];
1164
1259
  const issues = [{ ruleId: "JsTs.FileName", file: "src/app.ts", line: "10" }];
1165
1260
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1166
- const result = service.filterIssuesByOverrides(issues, specs, 1);
1261
+ const result = service.filterIssuesByOverrides(issues, specs, undefined, 1);
1167
1262
  expect(result).toHaveLength(0);
1168
1263
  expect(consoleSpy).toHaveBeenCalled();
1169
1264
  consoleSpy.mockRestore();
@@ -1192,7 +1287,7 @@ const MAX_COUNT = 100;
1192
1287
  ];
1193
1288
  const issues = [{ ruleId: "Other.Rule", file: "src/app.ts" }];
1194
1289
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1195
- service.filterIssuesByOverrides(issues, specs, 3);
1290
+ service.filterIssuesByOverrides(issues, specs, undefined, 3);
1196
1291
  expect(consoleSpy).toHaveBeenCalled();
1197
1292
  consoleSpy.mockRestore();
1198
1293
  });
@@ -1558,14 +1653,16 @@ const MAX_COUNT = 100;
1558
1653
 
1559
1654
  describe("extractExamples - bad type", () => {
1560
1655
  it("should extract bad examples", () => {
1561
- const content = `### Bad
1656
+ const content = `#### Bad: 不合理的命名
1562
1657
 
1563
1658
  \`\`\`ts
1564
1659
  const bad_name = 1;
1565
1660
  \`\`\``;
1566
1661
  const examples = (service as any).extractExamples(content);
1567
1662
  expect(examples).toHaveLength(1);
1568
- expect(examples[0].type).toBe("bad");
1663
+ expect(examples[0].content).toHaveLength(1);
1664
+ expect(examples[0].content[0].type).toBe("bad");
1665
+ expect(examples[0].content[0].title).toBe("不合理的命名");
1569
1666
  });
1570
1667
  });
1571
1668
 
@@ -1617,4 +1714,125 @@ const bad_name = 1;
1617
1714
  expect(result[0].file).toBe("user.controller.ts");
1618
1715
  });
1619
1716
  });
1717
+
1718
+ describe("extractExamples - #### level with colon", () => {
1719
+ it("should extract examples from #### Good: / #### Bad: format without group", () => {
1720
+ const content = `#### Good: 合理的常量命名
1721
+
1722
+ \`\`\`javascript
1723
+ const MAX_COUNT = 100;
1724
+ \`\`\`
1725
+
1726
+ #### Bad: 不合理的常量命名
1727
+
1728
+ \`\`\`javascript
1729
+ const maxCount = 100;
1730
+ \`\`\``;
1731
+ const examples = (service as any).extractExamples(content);
1732
+ expect(examples).toHaveLength(1);
1733
+ expect(examples[0].title).toBe("");
1734
+ expect(examples[0].description).toBe("");
1735
+ expect(examples[0].content).toHaveLength(2);
1736
+ expect(examples[0].content[0]).toEqual({
1737
+ title: "合理的常量命名",
1738
+ type: "good",
1739
+ description: "const MAX_COUNT = 100;",
1740
+ });
1741
+ expect(examples[0].content[1]).toEqual({
1742
+ title: "不合理的常量命名",
1743
+ type: "bad",
1744
+ description: "const maxCount = 100;",
1745
+ });
1746
+ });
1747
+
1748
+ it("should parse full rule with #### examples", () => {
1749
+ const mockContent = `# 基础代码规范 \`[JsTs.Base]\`
1750
+
1751
+ ## 常量名使用大写加下划线命名 \`[JsTs.Base.ConstUpperCase]\`
1752
+
1753
+ - 不检查 nodejs 的导包定义
1754
+ - 常量检查只需检查 const 声明的静态值
1755
+
1756
+ #### Good: 合理的常量命名
1757
+
1758
+ \`\`\`javascript
1759
+ const MAX_COUNT = 100;
1760
+ \`\`\`
1761
+
1762
+ #### Bad: 不合理的常量命名
1763
+
1764
+ \`\`\`javascript
1765
+ const maxCount = 100;
1766
+ \`\`\``;
1767
+
1768
+ const spec = service.parseSpecFile("js&ts.base.md", mockContent);
1769
+ expect(spec).not.toBeNull();
1770
+ expect(spec!.rules).toHaveLength(2);
1771
+ expect(spec!.rules[1].id).toBe("JsTs.Base.ConstUpperCase");
1772
+ expect(spec!.rules[1].description).toContain("不检查 nodejs");
1773
+ expect(spec!.rules[1].description).not.toContain("Good");
1774
+ expect(spec!.rules[1].examples).toHaveLength(1);
1775
+ expect(spec!.rules[1].examples[0].content).toHaveLength(2);
1776
+ expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
1777
+ expect(spec!.rules[1].examples[0].content[1].type).toBe("bad");
1778
+ });
1779
+
1780
+ it("should parse multiple example groups with ### Example:", () => {
1781
+ const mockContent = `# 基础代码规范 \`[JsTs.Base]\`
1782
+
1783
+ ## 常量名使用大写加下划线命名 \`[JsTs.Base.ConstUpperCase]\`
1784
+
1785
+ - 不检查 nodejs 的导包定义
1786
+ - 常量检查只需检查 const 声明的静态值
1787
+
1788
+ ### Example: 下面的明明规则说明
1789
+
1790
+ #### Good: 合理的常量命名
1791
+
1792
+ \`\`\`javascript
1793
+ const MAX_COUNT = 100;
1794
+ \`\`\`
1795
+
1796
+ #### Bad: 不合理的常量命名
1797
+
1798
+ \`\`\`javascript
1799
+ const maxCount = 100;
1800
+ \`\`\`
1801
+
1802
+ ### Example: 另一种场景
1803
+
1804
+ #### Good: 枚举值命名
1805
+
1806
+ \`\`\`javascript
1807
+ const STATUS_ACTIVE = "active";
1808
+ \`\`\`
1809
+
1810
+ #### Bad: 枚举值小驼峰
1811
+
1812
+ \`\`\`javascript
1813
+ const statusActive = "active";
1814
+ \`\`\``;
1815
+
1816
+ const spec = service.parseSpecFile("js&ts.base.md", mockContent);
1817
+ expect(spec).not.toBeNull();
1818
+ expect(spec!.rules[1].id).toBe("JsTs.Base.ConstUpperCase");
1819
+ expect(spec!.rules[1].description).toContain("不检查 nodejs");
1820
+ expect(spec!.rules[1].description).not.toContain("Example");
1821
+ expect(spec!.rules[1].examples).toHaveLength(2);
1822
+
1823
+ // 第一组
1824
+ expect(spec!.rules[1].examples[0].title).toBe("Example");
1825
+ expect(spec!.rules[1].examples[0].description).toBe("下面的明明规则说明");
1826
+ expect(spec!.rules[1].examples[0].content).toHaveLength(2);
1827
+ expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
1828
+ expect(spec!.rules[1].examples[0].content[1].type).toBe("bad");
1829
+
1830
+ // 第二组
1831
+ expect(spec!.rules[1].examples[1].title).toBe("Example");
1832
+ expect(spec!.rules[1].examples[1].description).toBe("另一种场景");
1833
+ expect(spec!.rules[1].examples[1].content).toHaveLength(2);
1834
+ expect(spec!.rules[1].examples[1].content[0].type).toBe("good");
1835
+ expect(spec!.rules[1].examples[1].content[1].type).toBe("bad");
1836
+ });
1837
+ });
1620
1838
  });
@@ -12,9 +12,9 @@ import { readdir, readFile, mkdir, access, writeFile, unlink } from "fs/promises
12
12
  import { join, basename } from "path";
13
13
  import { homedir } from "os";
14
14
  import { execSync, execFileSync } from "child_process";
15
- import micromatch from "micromatch";
16
- import { ReviewSpec, ReviewRule, RuleExample, Severity } from "./types";
17
- import { extractGlobsFromIncludes } from "../review-includes-filter";
15
+ import { ReviewSpec, ReviewRule, RuleExample, RuleContent, Severity } from "./types";
16
+ import { matchIncludes } from "../review-includes-filter";
17
+ import { buildSpecsSection as buildSpecsSectionPrompt } from "../prompt/specs-section";
18
18
 
19
19
  export class ReviewSpecService {
20
20
  constructor(protected readonly gitProvider?: GitProviderService) {}
@@ -606,7 +606,9 @@ export class ReviewSpecService {
606
606
 
607
607
  // 提取描述:在第一个例子之前的文本
608
608
  let description = ruleContent;
609
- const firstExampleIndex = ruleContent.search(/(?:^|\n)###\s+(?:good|bad)/i);
609
+ const firstExampleIndex = ruleContent.search(
610
+ /(?:^|\n)(?:####\s+(?:good|bad)|###\s+Example:)/i,
611
+ );
610
612
  if (firstExampleIndex !== -1) {
611
613
  description = ruleContent.slice(0, firstExampleIndex).trim();
612
614
  } else {
@@ -684,27 +686,64 @@ export class ReviewSpecService {
684
686
 
685
687
  protected extractExamples(content: string): RuleExample[] {
686
688
  const examples: RuleExample[] = [];
687
- const sections = content.split(/(?:^|\n)###\s+/);
688
689
 
689
- for (const section of sections) {
690
- const trimmedSection = section.trim();
691
- if (!trimmedSection) continue;
690
+ // ### Example: 分组
691
+ const groupSections = content.split(/(?:^|\n)(?=###\s+Example:)/);
692
+
693
+ for (const groupSection of groupSections) {
694
+ const trimmedGroup = groupSection.trim();
695
+ if (!trimmedGroup) continue;
692
696
 
693
- let type: "good" | "bad" | null = null;
694
- if (/^good\b/i.test(trimmedSection)) {
695
- type = "good";
696
- } else if (/^bad\b/i.test(trimmedSection)) {
697
- type = "bad";
697
+ // 提取分组描述,如 "### Example: 命名规则说明"
698
+ let exampleTitle = "";
699
+ let exampleDescription = "";
700
+ const groupMatch = trimmedGroup.match(/^###\s+Example\s*[::]\s*(.+)/i);
701
+ if (groupMatch) {
702
+ exampleTitle = "Example";
703
+ exampleDescription = groupMatch[1].trim();
698
704
  }
699
705
 
700
- if (!type) continue;
706
+ // 在分组内按 #### 提取 Good/Bad
707
+ const ruleContents: RuleContent[] = [];
708
+ const sections = trimmedGroup.split(/(?:^|\n)####\s+/);
709
+
710
+ for (const section of sections) {
711
+ const trimmedSection = section.trim();
712
+ if (!trimmedSection) continue;
713
+
714
+ let type: "good" | "bad" | null = null;
715
+ let contentTitle = "";
716
+ if (/^good:/i.test(trimmedSection)) {
717
+ type = "good";
718
+ contentTitle = trimmedSection.match(/^good:\s*(.+)/i)?.[1]?.trim() ?? "";
719
+ } else if (/^bad:/i.test(trimmedSection)) {
720
+ type = "bad";
721
+ contentTitle = trimmedSection.match(/^bad:\s*(.+)/i)?.[1]?.trim() ?? "";
722
+ }
723
+
724
+ if (!type) continue;
725
+
726
+ // 提取代码块作为 description
727
+ const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
728
+ let codeMatch;
729
+ const codeParts: string[] = [];
730
+ while ((codeMatch = codeBlockRegex.exec(trimmedSection)) !== null) {
731
+ codeParts.push(codeMatch[2].trim());
732
+ }
733
+
734
+ ruleContents.push({
735
+ title: contentTitle,
736
+ type,
737
+ description: codeParts.join("\n\n"),
738
+ });
739
+ }
701
740
 
702
- const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
703
- let codeMatch;
704
- while ((codeMatch = codeBlockRegex.exec(trimmedSection)) !== null) {
705
- const lang = codeMatch[1] || "text";
706
- const code = codeMatch[2].trim();
707
- examples.push({ lang, code, type });
741
+ if (ruleContents.length > 0) {
742
+ examples.push({
743
+ title: exampleTitle,
744
+ description: exampleDescription,
745
+ content: ruleContents,
746
+ });
708
747
  }
709
748
  }
710
749
 
@@ -748,10 +787,12 @@ export class ReviewSpecService {
748
787
  * 根据 spec 的 includes 配置过滤 issues
749
788
  * 只保留文件名匹配对应 spec includes 模式的 issues
750
789
  * 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
790
+ * 支持 `added|`/`modified|`/`deleted|` 前缀语法
751
791
  */
752
792
  filterIssuesByIncludes<T extends { file: string; ruleId: string }>(
753
793
  issues: T[],
754
794
  specs: ReviewSpec[],
795
+ changedFiles?: ChangedFileCollection,
755
796
  ): T[] {
756
797
  // 构建 spec filename -> includes 的映射
757
798
  const specIncludesMap = new Map<string, string[]>();
@@ -771,14 +812,9 @@ export class ReviewSpecService {
771
812
  return true;
772
813
  }
773
814
 
774
- // 检查文件是否匹配 includes 模式(转换为纯 glob,避免 status| 前缀和 code-* 空串传入 micromatch)
775
- const globs = extractGlobsFromIncludes(includes);
776
- if (globs.length === 0) return true;
777
- const matches = micromatch.isMatch(issue.file, globs, { matchBase: true });
778
- if (!matches) {
779
- // console.log(` Issue [${issue.ruleId}] 在文件 ${issue.file} 不匹配 includes 模式,跳过`);
780
- }
781
- return matches;
815
+ // 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
816
+ const fileStatus = changedFiles?.getStatus(issue.file);
817
+ return matchIncludes(includes, issue.file, fileStatus);
782
818
  });
783
819
  }
784
820
 
@@ -816,6 +852,7 @@ export class ReviewSpecService {
816
852
  filterIssuesByOverrides<T extends { ruleId: string; file?: string }>(
817
853
  issues: T[],
818
854
  specs: ReviewSpec[],
855
+ changedFiles?: ChangedFileCollection,
819
856
  verbose?: VerboseLevel,
820
857
  ): T[] {
821
858
  // ========== 阶段1: 收集 spec -> overrides 的映射(保留作用域信息) ==========
@@ -887,10 +924,9 @@ export class ReviewSpecService {
887
924
  if (scoped.includes.length === 0) {
888
925
  return true;
889
926
  }
890
- // 使用 micromatch 检查文件是否匹配 includes 模式(转换为纯 glob
891
- const globs = extractGlobsFromIncludes(scoped.includes);
892
- if (globs.length === 0) return true;
893
- return issueFile && micromatch.isMatch(issueFile, globs, { matchBase: true });
927
+ // 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
928
+ const fileStatus = changedFiles?.getStatus(issueFile);
929
+ return matchIncludes(scoped.includes, issueFile, fileStatus);
894
930
  });
895
931
 
896
932
  if (matched) {
@@ -1011,29 +1047,7 @@ export class ReviewSpecService {
1011
1047
  * 构建 specs 的 prompt 部分
1012
1048
  */
1013
1049
  buildSpecsSection(specs: ReviewSpec[]): string {
1014
- return specs
1015
- .map((spec) => {
1016
- const firstRule = spec.rules[0];
1017
- const rulesText = spec.rules
1018
- .slice(1)
1019
- .map((rule) => {
1020
- let text = `#### [${rule.id}] ${rule.title}\n`;
1021
- if (rule.description) {
1022
- text += `${rule.description}\n`;
1023
- }
1024
- if (rule.examples.length > 0) {
1025
- for (const example of rule.examples) {
1026
- text += `##### ${example.type === "good" ? "推荐做法 (Good)" : "不推荐做法 (Bad)"}\n`;
1027
- text += `\`\`\`${example.lang}\n${example.code}\n\`\`\`\n`;
1028
- }
1029
- }
1030
- return text;
1031
- })
1032
- .join("\n");
1033
-
1034
- return `### ${firstRule.title}\n- 规范文件: ${spec.filename}\n- 适用扩展名: ${spec.extensions.join(", ")}\n\n${rulesText}`;
1035
- })
1036
- .join("\n\n-------------------\n\n");
1050
+ return buildSpecsSectionPrompt(specs);
1037
1051
  }
1038
1052
 
1039
1053
  /**
@@ -24,9 +24,15 @@ export interface ReviewSpec {
24
24
  }
25
25
 
26
26
  export interface RuleExample {
27
- lang: string;
28
- code: string;
27
+ title: string;
28
+ description: string;
29
+ content: RuleContent[];
30
+ }
31
+
32
+ export interface RuleContent {
33
+ title: string;
29
34
  type: "good" | "bad";
35
+ description: string;
30
36
  }
31
37
 
32
38
  export interface ReviewRule {
@@ -650,6 +650,50 @@ describe("ReviewService", () => {
650
650
  expect(result.issues).toHaveLength(1);
651
651
  expect(result.stats).toBeDefined();
652
652
  });
653
+
654
+ it("should filter merge commits before getFileContents when verifyFixes enabled", async () => {
655
+ const existingResult = { issues: [{ file: "a.ts", line: "1", ruleId: "R1" }], summary: [] };
656
+ service._reviewReportService.formatStatsTerminal = vi.fn().mockReturnValue("stats") as any;
657
+ gitProvider.listPullReviews.mockResolvedValue([] as any);
658
+ gitProvider.listPullReviewComments.mockResolvedValue([] as any);
659
+ gitProvider.getPullRequest.mockResolvedValue({ head: { sha: "head1234" } } as any);
660
+ gitProvider.getPullRequestFiles.mockResolvedValue([
661
+ { filename: "a.ts", status: "modified" },
662
+ ] as any);
663
+ gitProvider.getPullRequestCommits.mockResolvedValue([
664
+ { sha: "merge1111", commit: { message: "Merge branch 'main' into feature" } },
665
+ { sha: "feat22222", commit: { message: "feat: add logic" } },
666
+ ] as any);
667
+
668
+ vi.spyOn(ReviewResultModel, "loadFromPr").mockResolvedValue(
669
+ ReviewResultModel.create(
670
+ new PullRequestModel(gitProvider as any, "o", "r", 1),
671
+ existingResult as any,
672
+ service._resultModelDeps,
673
+ ),
674
+ );
675
+ const getFileContentsSpy = vi
676
+ .spyOn(service._sourceResolver, "getFileContents")
677
+ .mockResolvedValue(new Map() as any);
678
+
679
+ const context = {
680
+ owner: "o",
681
+ repo: "r",
682
+ prNumber: 1,
683
+ ci: false,
684
+ dryRun: false,
685
+ verifyFixes: true,
686
+ specSources: ["/spec/dir"],
687
+ showAll: false,
688
+ };
689
+
690
+ await service.executeCollectOnly(context);
691
+
692
+ expect(getFileContentsSpy).toHaveBeenCalled();
693
+ const passedCommits = getFileContentsSpy.mock.calls[0][3] as any[];
694
+ expect(passedCommits).toHaveLength(1);
695
+ expect(passedCommits[0].sha).toBe("feat22222");
696
+ });
653
697
  });
654
698
 
655
699
  describe("ReviewService.execute - flush mode", () => {
@@ -3,7 +3,6 @@ import {
3
3
  PullRequestCommit,
4
4
  type LLMMode,
5
5
  LlmProxyService,
6
- type VerboseLevel,
7
6
  shouldLog,
8
7
  GitSdkService,
9
8
  } from "@spaceflow/core";
@@ -281,7 +280,7 @@ export class ReviewService {
281
280
  const { commits, fileContents, changedFiles, isDirectFileMode, context } = opts;
282
281
  const { verbose } = context;
283
282
 
284
- let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs);
283
+ let filtered = this.reviewSpecService.filterIssuesByIncludes(issues, specs, changedFiles);
285
284
  if (shouldLog(verbose, 1)) {
286
285
  console.log(` 应用 includes 过滤后: ${filtered.length} 个问题`);
287
286
  }
@@ -291,7 +290,12 @@ export class ReviewService {
291
290
  console.log(` 应用规则存在性过滤后: ${filtered.length} 个问题`);
292
291
  }
293
292
 
294
- filtered = this.reviewSpecService.filterIssuesByOverrides(filtered, specs, verbose);
293
+ filtered = this.reviewSpecService.filterIssuesByOverrides(
294
+ filtered,
295
+ specs,
296
+ changedFiles,
297
+ verbose,
298
+ );
295
299
 
296
300
  // 变更行过滤
297
301
  if (shouldLog(verbose, 3)) {
@@ -496,7 +500,13 @@ export class ReviewService {
496
500
  }
497
501
 
498
502
  // 2. 获取 commits 并填充 author 信息
499
- const commits = await prModel.getCommits();
503
+ const allCommits = await prModel.getCommits();
504
+ const commits = context.showAll
505
+ ? allCommits
506
+ : allCommits.filter((c) => !/^merge\b/i.test(c.commit?.message || ""));
507
+ if (allCommits.length !== commits.length && shouldLog(verbose, 1)) {
508
+ console.log(` 跳过 Merge Commits: ${allCommits.length} -> ${commits.length} 个`);
509
+ }
500
510
  resultModel.issues = await this.contextBuilder.fillIssueAuthors(
501
511
  resultModel.issues,
502
512
  commits,
@@ -525,6 +535,7 @@ export class ReviewService {
525
535
  headSha,
526
536
  prNumber,
527
537
  false,
538
+ context.showAll,
528
539
  verbose,
529
540
  );
530
541
  resultModel.issues = await this.issueFilter.verifyAndUpdateIssues(
package/dist/551.js DELETED
@@ -1,9 +0,0 @@
1
- export const __rspack_esm_id = "551";
2
- export const __rspack_esm_ids = ["551"];
3
- export const __webpack_modules__ = {
4
- 946(module) {
5
- module.exports = import("micromatch").then(function(module) { return module; });
6
-
7
- },
8
-
9
- };