@spaceflow/review 1.0.0 → 3.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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@1.0.0...@spaceflow/review@2.0.0) (2026-04-13)
4
+
5
+ ### 代码重构
6
+
7
+ * **review:** 优化 Example 标题和描述解析逻辑 ([decb54f](https://github.com/Lydanne/spaceflow/commit/decb54f6832acaeddd83ccfb6a3439c47294f11d))
8
+
9
+ ### 其他修改
10
+
11
+ * **review-summary:** released version 1.0.0 [no ci] ([742d53e](https://github.com/Lydanne/spaceflow/commit/742d53efb7f16e33c50d9b1c4b9e31a7c0e8da21))
12
+
13
+ ## [1.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.83.0...@spaceflow/review@1.0.0) (2026-04-13)
14
+
15
+ ### ⚠ BREAKING CHANGES
16
+
17
+ * **review:** 重构 example 数据结构,支持 `### Example:` 分组和 `#### Good:/Bad:` 标题语法
18
+
19
+ ### 新特性
20
+
21
+ * **review:** 新增 includes 变更类型前缀语法支持 ([2f5655f](https://github.com/Lydanne/spaceflow/commit/2f5655fc414828ea3a0269fba04611d2bf2591a9))
22
+ * **review:** 重构 example 数据结构,支持 `### Example:` 分组和 `#### Good:/Bad:` 标题语法 ([e45bd5a](https://github.com/Lydanne/spaceflow/commit/e45bd5aab5e317b8f70a8b107c17aab6548c3296))
23
+
24
+ ### 其他修改
25
+
26
+ * **review-summary:** released version 0.52.0 [no ci] ([1d0cdac](https://github.com/Lydanne/spaceflow/commit/1d0cdacdbe12db98399e69fd53bbfdd23825cc10))
27
+
3
28
  ## [0.83.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.82.0...@spaceflow/review@0.83.0) (2026-04-10)
4
29
 
5
30
  ### 代码重构
package/dist/index.js CHANGED
@@ -305,7 +305,7 @@ const CODE_BLOCK_TYPES = [
305
305
  function formatExample(example) {
306
306
  let text = "";
307
307
  if (example.title) {
308
- text += `##### ${example.title}\n`;
308
+ text += `##### Example: ${example.title}\n`;
309
309
  }
310
310
  if (example.description) {
311
311
  text += `${example.description}\n`;
@@ -903,13 +903,18 @@ class ReviewSpecService {
903
903
  for (const groupSection of groupSections){
904
904
  const trimmedGroup = groupSection.trim();
905
905
  if (!trimmedGroup) continue;
906
- // 提取分组描述,如 "### Example: 命名规则说明"
906
+ // 提取分组标题和描述,如 "### Example: 函数行数"
907
907
  let exampleTitle = "";
908
908
  let exampleDescription = "";
909
909
  const groupMatch = trimmedGroup.match(/^###\s+Example\s*[::]\s*(.+)/i);
910
910
  if (groupMatch) {
911
- exampleTitle = "Example";
912
- exampleDescription = groupMatch[1].trim();
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
+ }
913
918
  }
914
919
  // 在分组内按 #### 提取 Good/Bad
915
920
  const ruleContents = [];
@@ -983,12 +988,12 @@ class ReviewSpecService {
983
988
  * 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
984
989
  * 支持 `added|`/`modified|`/`deleted|` 前缀语法
985
990
  */ filterIssuesByIncludes(issues, specs, changedFiles) {
986
- // 构建 spec filename -> includes 的映射
991
+ // 构建 rule.id -> includes 的映射(规则级优先,文件级兜底)
987
992
  const specIncludesMap = new Map();
988
993
  for (const spec of specs){
989
- // 从规则 ID 前缀推断 spec filename
990
994
  for (const rule of spec.rules){
991
- specIncludesMap.set(rule.id, spec.includes);
995
+ // 规则级 includes 覆盖文件级 includes,与 severity 优先级一致
996
+ specIncludesMap.set(rule.id, rule.includes ?? spec.includes);
992
997
  }
993
998
  }
994
999
  return issues.filter((issue)=>{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "1.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -31,7 +31,7 @@ export function buildSpecsSection(specs: ReviewSpec[]): string {
31
31
  function formatExample(example: RuleExample): string {
32
32
  let text = "";
33
33
  if (example.title) {
34
- text += `##### ${example.title}\n`;
34
+ text += `##### Example: ${example.title}\n`;
35
35
  }
36
36
  if (example.description) {
37
37
  text += `${example.description}\n`;
@@ -1674,8 +1674,8 @@ const bad_name = 1;
1674
1674
  });
1675
1675
  });
1676
1676
 
1677
- describe("filterIssuesByIncludes - rule-level includes", () => {
1678
- it("should use rule-level includes for filtering", () => {
1677
+ describe("filterIssuesByIncludes - includes priority", () => {
1678
+ it("should use rule-level includes over file-level includes", () => {
1679
1679
  const specs = [
1680
1680
  {
1681
1681
  filename: "nest.md",
@@ -1706,12 +1706,301 @@ const bad_name = 1;
1706
1706
  ];
1707
1707
  const issues = [
1708
1708
  { file: "user.model.ts", ruleId: "JsTs.Nest.Model" },
1709
+ { file: "order.model.ts", ruleId: "JsTs.Nest.Model" },
1709
1710
  { file: "user.controller.ts", ruleId: "JsTs.Nest" },
1711
+ { file: "order.controller.ts", ruleId: "JsTs.Nest" },
1712
+ { file: "user.service.ts", ruleId: "JsTs.Nest" },
1713
+ { file: "user.module.ts", ruleId: "JsTs.Nest" },
1714
+ { file: "user.model.ts", ruleId: "JsTs.Nest" }, // model 文件但 ruleId 是 Nest(文件级)
1710
1715
  ];
1711
1716
  const result = service.filterIssuesByIncludes(issues, specs);
1712
- // spec.includes *.controller.ts,user.model.ts 不匹配
1713
- expect(result).toHaveLength(1);
1714
- expect(result[0].file).toBe("user.controller.ts");
1717
+ // JsTs.Nest.Model 规则级 *.model.ts,user.model.ts / order.model.ts 匹配 → 保留
1718
+ // JsTs.Nest → 文件级 *.controller.ts,user.controller.ts / order.controller.ts 匹配 → 保留
1719
+ // user.service.ts / user.module.ts 不匹配文件级 *.controller.ts → 过滤
1720
+ // user.model.ts(ruleId=Nest) → 文件级 *.controller.ts,不匹配 → 过滤
1721
+ expect(result).toHaveLength(4);
1722
+ expect(result.map((i) => i.file)).toEqual([
1723
+ "user.model.ts",
1724
+ "order.model.ts",
1725
+ "user.controller.ts",
1726
+ "order.controller.ts",
1727
+ ]);
1728
+ });
1729
+
1730
+ it("should fall back to file-level includes when rule has no includes", () => {
1731
+ const specs = [
1732
+ {
1733
+ filename: "nest.md",
1734
+ extensions: ["ts"],
1735
+ type: "nest",
1736
+ content: "",
1737
+ overrides: [],
1738
+ severity: "error" as const,
1739
+ includes: ["*.controller.ts"],
1740
+ rules: [
1741
+ {
1742
+ id: "JsTs.Nest",
1743
+ title: "Nest",
1744
+ description: "",
1745
+ examples: [],
1746
+ overrides: [],
1747
+ },
1748
+ {
1749
+ id: "JsTs.Nest.DirStructure",
1750
+ title: "DirStructure",
1751
+ description: "",
1752
+ examples: [],
1753
+ overrides: [],
1754
+ // 无 rule.includes → 回退到 spec.includes
1755
+ },
1756
+ ],
1757
+ },
1758
+ ];
1759
+ const issues = [
1760
+ { file: "user.controller.ts", ruleId: "JsTs.Nest.DirStructure" },
1761
+ { file: "order.controller.ts", ruleId: "JsTs.Nest.DirStructure" },
1762
+ { file: "user.service.ts", ruleId: "JsTs.Nest.DirStructure" },
1763
+ { file: "user.module.ts", ruleId: "JsTs.Nest.DirStructure" },
1764
+ { file: "user.controller.ts", ruleId: "JsTs.Nest" },
1765
+ { file: "user.service.ts", ruleId: "JsTs.Nest" },
1766
+ ];
1767
+ const result = service.filterIssuesByIncludes(issues, specs);
1768
+ // DirStructure 无规则级 includes → 回退文件级 *.controller.ts
1769
+ // user.controller.ts / order.controller.ts 匹配 → 保留
1770
+ // user.service.ts / user.module.ts 不匹配 → 过滤
1771
+ // JsTs.Nest 同样回退文件级,user.controller.ts 匹配 → 保留,user.service.ts 不匹配 → 过滤
1772
+ expect(result).toHaveLength(3);
1773
+ expect(result.map((i) => i.file)).toEqual([
1774
+ "user.controller.ts",
1775
+ "order.controller.ts",
1776
+ "user.controller.ts",
1777
+ ]);
1778
+ });
1779
+
1780
+ it("should use rule-level includes even when it narrows the file-level scope", () => {
1781
+ const specs = [
1782
+ {
1783
+ filename: "js&ts.md",
1784
+ extensions: ["ts"],
1785
+ type: "base",
1786
+ content: "",
1787
+ overrides: [],
1788
+ severity: "error" as const,
1789
+ includes: ["*.ts"], // 文件级:所有 ts 文件
1790
+ rules: [
1791
+ {
1792
+ id: "JsTs.Base",
1793
+ title: "Base",
1794
+ description: "",
1795
+ examples: [],
1796
+ overrides: [],
1797
+ },
1798
+ {
1799
+ id: "JsTs.Base.TestRule",
1800
+ title: "TestRule",
1801
+ description: "",
1802
+ examples: [],
1803
+ overrides: [],
1804
+ includes: ["*.spec.ts"], // 规则级:仅 spec 文件(收窄)
1805
+ },
1806
+ ],
1807
+ },
1808
+ ];
1809
+ const issues = [
1810
+ { file: "app.spec.ts", ruleId: "JsTs.Base.TestRule" },
1811
+ { file: "utils.spec.ts", ruleId: "JsTs.Base.TestRule" },
1812
+ { file: "app.ts", ruleId: "JsTs.Base.TestRule" },
1813
+ { file: "utils.ts", ruleId: "JsTs.Base.TestRule" },
1814
+ { file: "app.ts", ruleId: "JsTs.Base" },
1815
+ { file: "utils.ts", ruleId: "JsTs.Base" },
1816
+ { file: "app.spec.ts", ruleId: "JsTs.Base" },
1817
+ ];
1818
+ const result = service.filterIssuesByIncludes(issues, specs);
1819
+ // JsTs.Base.TestRule → 规则级 *.spec.ts,app.spec.ts / utils.spec.ts 匹配 → 保留
1820
+ // app.ts / utils.ts 不匹配 *.spec.ts → 过滤
1821
+ // JsTs.Base → 文件级 *.ts,app.ts / utils.ts / app.spec.ts 匹配 → 保留
1822
+ expect(result).toHaveLength(5);
1823
+ expect(result.map((i) => i.file)).toEqual([
1824
+ "app.spec.ts",
1825
+ "utils.spec.ts",
1826
+ "app.ts",
1827
+ "utils.ts",
1828
+ "app.spec.ts",
1829
+ ]);
1830
+ });
1831
+
1832
+ it("should use rule-level includes even when it widens the file-level scope", () => {
1833
+ const specs = [
1834
+ {
1835
+ filename: "nest.md",
1836
+ extensions: ["ts"],
1837
+ type: "nest",
1838
+ content: "",
1839
+ overrides: [],
1840
+ severity: "error" as const,
1841
+ includes: ["*.controller.ts"], // 文件级:仅 controller
1842
+ rules: [
1843
+ {
1844
+ id: "JsTs.Nest",
1845
+ title: "Nest",
1846
+ description: "",
1847
+ examples: [],
1848
+ overrides: [],
1849
+ },
1850
+ {
1851
+ id: "JsTs.Nest.AllFiles",
1852
+ title: "AllFiles",
1853
+ description: "",
1854
+ examples: [],
1855
+ overrides: [],
1856
+ includes: ["*.ts"], // 规则级:所有 ts 文件(放宽)
1857
+ },
1858
+ ],
1859
+ },
1860
+ ];
1861
+ const issues = [
1862
+ { file: "user.service.ts", ruleId: "JsTs.Nest.AllFiles" },
1863
+ { file: "user.controller.ts", ruleId: "JsTs.Nest.AllFiles" },
1864
+ { file: "user.module.ts", ruleId: "JsTs.Nest.AllFiles" },
1865
+ { file: "user.controller.ts", ruleId: "JsTs.Nest" },
1866
+ { file: "order.controller.ts", ruleId: "JsTs.Nest" },
1867
+ { file: "user.service.ts", ruleId: "JsTs.Nest" },
1868
+ { file: "user.module.ts", ruleId: "JsTs.Nest" },
1869
+ ];
1870
+ const result = service.filterIssuesByIncludes(issues, specs);
1871
+ // JsTs.Nest.AllFiles → 规则级 *.ts,全部匹配 → 保留 3 个
1872
+ // JsTs.Nest → 文件级 *.controller.ts,user.controller.ts / order.controller.ts 匹配 → 保留
1873
+ // user.service.ts / user.module.ts 不匹配 → 过滤
1874
+ expect(result).toHaveLength(5);
1875
+ expect(result.map((i) => i.file)).toEqual([
1876
+ "user.service.ts",
1877
+ "user.controller.ts",
1878
+ "user.module.ts",
1879
+ "user.controller.ts",
1880
+ "order.controller.ts",
1881
+ ]);
1882
+ });
1883
+
1884
+ it("should support status prefix in rule-level includes", () => {
1885
+ const specs = [
1886
+ {
1887
+ filename: "nest.md",
1888
+ extensions: ["ts"],
1889
+ type: "nest",
1890
+ content: "",
1891
+ overrides: [],
1892
+ severity: "error" as const,
1893
+ includes: ["*.controller.ts"], // 文件级:无前缀
1894
+ rules: [
1895
+ {
1896
+ id: "JsTs.Nest",
1897
+ title: "Nest",
1898
+ description: "",
1899
+ examples: [],
1900
+ overrides: [],
1901
+ },
1902
+ {
1903
+ id: "JsTs.Nest.Model",
1904
+ title: "Model",
1905
+ description: "",
1906
+ examples: [],
1907
+ overrides: [],
1908
+ includes: ["added|*.model.ts", "modified|*.dto.ts"], // 规则级:added 的 model + modified 的 dto
1909
+ },
1910
+ ],
1911
+ },
1912
+ ];
1913
+ const changedFiles = {
1914
+ getStatus: (file: string) => {
1915
+ if (file === "user.model.ts") return "added";
1916
+ if (file === "order.model.ts") return "modified";
1917
+ if (file === "user.dto.ts") return "modified";
1918
+ if (file === "order.dto.ts") return "added";
1919
+ return "modified";
1920
+ },
1921
+ } as any;
1922
+ const issues = [
1923
+ { file: "user.model.ts", ruleId: "JsTs.Nest.Model" }, // added + *.model.ts → 匹配
1924
+ { file: "order.model.ts", ruleId: "JsTs.Nest.Model" }, // modified + *.model.ts → 不匹配 added|
1925
+ { file: "user.dto.ts", ruleId: "JsTs.Nest.Model" }, // modified + *.dto.ts → 匹配 modified|
1926
+ { file: "order.dto.ts", ruleId: "JsTs.Nest.Model" }, // added + *.dto.ts → 不匹配 modified|
1927
+ { file: "user.controller.ts", ruleId: "JsTs.Nest" }, // 文件级 *.controller.ts → 匹配
1928
+ { file: "user.service.ts", ruleId: "JsTs.Nest" }, // 文件级 *.controller.ts → 不匹配
1929
+ ];
1930
+ const result = service.filterIssuesByIncludes(issues, specs, changedFiles);
1931
+ expect(result).toHaveLength(3);
1932
+ expect(result.map((i) => i.file)).toEqual([
1933
+ "user.model.ts",
1934
+ "user.dto.ts",
1935
+ "user.controller.ts",
1936
+ ]);
1937
+
1938
+ // 全部 modified 时,added|*.model.ts 不匹配,modified|*.dto.ts 匹配
1939
+ const changedFiles2 = {
1940
+ getStatus: () => "modified",
1941
+ } as any;
1942
+ const result2 = service.filterIssuesByIncludes(issues, specs, changedFiles2);
1943
+ // user.model.ts: modified, added|*.model.ts 不匹配 → 过滤
1944
+ // order.model.ts: modified, added|*.model.ts 不匹配 → 过滤
1945
+ // user.dto.ts: modified, modified|*.dto.ts 匹配 → 保留
1946
+ // order.dto.ts: modified, modified|*.dto.ts 匹配 → 保留
1947
+ // user.controller.ts: 文件级匹配 → 保留
1948
+ // user.service.ts: 文件级不匹配 → 过滤
1949
+ expect(result2).toHaveLength(3);
1950
+ expect(result2.map((i) => i.file)).toEqual([
1951
+ "user.dto.ts",
1952
+ "order.dto.ts",
1953
+ "user.controller.ts",
1954
+ ]);
1955
+ });
1956
+
1957
+ it("should use empty rule-level includes to remove file-level restriction", () => {
1958
+ const specs = [
1959
+ {
1960
+ filename: "nest.md",
1961
+ extensions: ["ts"],
1962
+ type: "nest",
1963
+ content: "",
1964
+ overrides: [],
1965
+ severity: "error" as const,
1966
+ includes: ["*.controller.ts"], // 文件级:仅 controller
1967
+ rules: [
1968
+ {
1969
+ id: "JsTs.Nest",
1970
+ title: "Nest",
1971
+ description: "",
1972
+ examples: [],
1973
+ overrides: [],
1974
+ },
1975
+ {
1976
+ id: "JsTs.Nest.Global",
1977
+ title: "Global",
1978
+ description: "",
1979
+ examples: [],
1980
+ overrides: [],
1981
+ includes: [], // 规则级:空数组 = 无限制(覆盖文件级)
1982
+ },
1983
+ ],
1984
+ },
1985
+ ];
1986
+ const issues = [
1987
+ { file: "user.service.ts", ruleId: "JsTs.Nest.Global" },
1988
+ { file: "user.controller.ts", ruleId: "JsTs.Nest.Global" },
1989
+ { file: "user.module.ts", ruleId: "JsTs.Nest.Global" },
1990
+ { file: "user.service.ts", ruleId: "JsTs.Nest" },
1991
+ { file: "user.controller.ts", ruleId: "JsTs.Nest" },
1992
+ { file: "user.module.ts", ruleId: "JsTs.Nest" },
1993
+ ];
1994
+ const result = service.filterIssuesByIncludes(issues, specs);
1995
+ // JsTs.Nest.Global → 规则级 includes=[] (空=无限制),全部保留 3 个
1996
+ // JsTs.Nest → 文件级 *.controller.ts,仅 user.controller.ts 匹配 → 保留
1997
+ expect(result).toHaveLength(4);
1998
+ expect(result.map((i) => i.file)).toEqual([
1999
+ "user.service.ts",
2000
+ "user.controller.ts",
2001
+ "user.module.ts",
2002
+ "user.controller.ts",
2003
+ ]);
1715
2004
  });
1716
2005
  });
1717
2006
 
@@ -1821,18 +2110,55 @@ const statusActive = "active";
1821
2110
  expect(spec!.rules[1].examples).toHaveLength(2);
1822
2111
 
1823
2112
  // 第一组
1824
- expect(spec!.rules[1].examples[0].title).toBe("Example");
1825
- expect(spec!.rules[1].examples[0].description).toBe("下面的明明规则说明");
2113
+ expect(spec!.rules[1].examples[0].title).toBe("下面的明明规则说明");
2114
+ expect(spec!.rules[1].examples[0].description).toBe("");
1826
2115
  expect(spec!.rules[1].examples[0].content).toHaveLength(2);
1827
2116
  expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
1828
2117
  expect(spec!.rules[1].examples[0].content[1].type).toBe("bad");
1829
2118
 
1830
2119
  // 第二组
1831
- expect(spec!.rules[1].examples[1].title).toBe("Example");
1832
- expect(spec!.rules[1].examples[1].description).toBe("另一种场景");
2120
+ expect(spec!.rules[1].examples[1].title).toBe("另一种场景");
2121
+ expect(spec!.rules[1].examples[1].description).toBe("");
1833
2122
  expect(spec!.rules[1].examples[1].content).toHaveLength(2);
1834
2123
  expect(spec!.rules[1].examples[1].content[0].type).toBe("good");
1835
2124
  expect(spec!.rules[1].examples[1].content[1].type).toBe("bad");
1836
2125
  });
2126
+
2127
+ it("should parse Example with title and description paragraph", () => {
2128
+ const mockContent = `# 基础代码规范 \`[JsTs.Base]\`
2129
+
2130
+ ## 函数行数限制 \`[JsTs.Base.FuncLines]\`
2131
+
2132
+ - 函数不能太长
2133
+
2134
+ ### Example: 函数行数
2135
+
2136
+ 例子说明 desc
2137
+
2138
+ #### Good: 函数不超过 200 行
2139
+
2140
+ \`\`\`javascript
2141
+ function getUserInfo() {
2142
+ // ... 小于等于 200
2143
+ }
2144
+ \`\`\`
2145
+
2146
+ #### Bad: 函数超过 200 行
2147
+
2148
+ \`\`\`javascript
2149
+ function getUserInfo() {
2150
+ // ... 大于 200
2151
+ }
2152
+ \`\`\``;
2153
+
2154
+ const spec = service.parseSpecFile("js&ts.base.md", mockContent);
2155
+ expect(spec).not.toBeNull();
2156
+ expect(spec!.rules[1].examples).toHaveLength(1);
2157
+ expect(spec!.rules[1].examples[0].title).toBe("函数行数");
2158
+ expect(spec!.rules[1].examples[0].description).toBe("例子说明 desc");
2159
+ expect(spec!.rules[1].examples[0].content).toHaveLength(2);
2160
+ expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
2161
+ expect(spec!.rules[1].examples[0].content[0].title).toBe("函数不超过 200 行");
2162
+ });
1837
2163
  });
1838
2164
  });
@@ -694,13 +694,18 @@ export class ReviewSpecService {
694
694
  const trimmedGroup = groupSection.trim();
695
695
  if (!trimmedGroup) continue;
696
696
 
697
- // 提取分组描述,如 "### Example: 命名规则说明"
697
+ // 提取分组标题和描述,如 "### Example: 函数行数"
698
698
  let exampleTitle = "";
699
699
  let exampleDescription = "";
700
700
  const groupMatch = trimmedGroup.match(/^###\s+Example\s*[::]\s*(.+)/i);
701
701
  if (groupMatch) {
702
- exampleTitle = "Example";
703
- exampleDescription = groupMatch[1].trim();
702
+ exampleTitle = groupMatch[1].trim();
703
+ // 提取标题行和第一个 #### 之间的文本作为 description
704
+ const afterTitle = trimmedGroup.slice(trimmedGroup.indexOf("\n")).trim();
705
+ const firstSubIdx = afterTitle.search(/(?:^|\n)####\s+/);
706
+ if (firstSubIdx > 0) {
707
+ exampleDescription = afterTitle.slice(0, firstSubIdx).trim();
708
+ }
704
709
  }
705
710
 
706
711
  // 在分组内按 #### 提取 Good/Bad
@@ -794,12 +799,12 @@ export class ReviewSpecService {
794
799
  specs: ReviewSpec[],
795
800
  changedFiles?: ChangedFileCollection,
796
801
  ): T[] {
797
- // 构建 spec filename -> includes 的映射
802
+ // 构建 rule.id -> includes 的映射(规则级优先,文件级兜底)
798
803
  const specIncludesMap = new Map<string, string[]>();
799
804
  for (const spec of specs) {
800
- // 从规则 ID 前缀推断 spec filename
801
805
  for (const rule of spec.rules) {
802
- specIncludesMap.set(rule.id, spec.includes);
806
+ // 规则级 includes 覆盖文件级 includes,与 severity 优先级一致
807
+ specIncludesMap.set(rule.id, rule.includes ?? spec.includes);
803
808
  }
804
809
  }
805
810