@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 +27 -0
- package/dist/index.js +214 -202
- package/package.json +1 -1
- package/src/changed-file-collection.ts +11 -0
- package/src/mcp/index.ts +23 -16
- package/src/prompt/specs-section.ts +47 -0
- package/src/review-includes-filter.spec.ts +83 -0
- package/src/review-includes-filter.ts +80 -0
- package/src/review-llm.ts +6 -8
- package/src/review-spec/review-spec.service.spec.ts +266 -11
- package/src/review-spec/review-spec.service.ts +75 -56
- package/src/review-spec/types.ts +8 -2
- package/src/review.service.ts +7 -3
- package/dist/551.js +0 -9
|
@@ -35,7 +35,7 @@ describe("ReviewSpecService", () => {
|
|
|
35
35
|
- 排除配置文件
|
|
36
36
|
- 排除测试文件
|
|
37
37
|
|
|
38
|
-
|
|
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]).
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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: [
|
|
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 =
|
|
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].
|
|
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,162 @@ 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("下面的明明规则说明");
|
|
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("另一种场景");
|
|
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
|
+
|
|
1838
|
+
it("should parse Example with title and description paragraph", () => {
|
|
1839
|
+
const mockContent = `# 基础代码规范 \`[JsTs.Base]\`
|
|
1840
|
+
|
|
1841
|
+
## 函数行数限制 \`[JsTs.Base.FuncLines]\`
|
|
1842
|
+
|
|
1843
|
+
- 函数不能太长
|
|
1844
|
+
|
|
1845
|
+
### Example: 函数行数
|
|
1846
|
+
|
|
1847
|
+
例子说明 desc
|
|
1848
|
+
|
|
1849
|
+
#### Good: 函数不超过 200 行
|
|
1850
|
+
|
|
1851
|
+
\`\`\`javascript
|
|
1852
|
+
function getUserInfo() {
|
|
1853
|
+
// ... 小于等于 200
|
|
1854
|
+
}
|
|
1855
|
+
\`\`\`
|
|
1856
|
+
|
|
1857
|
+
#### Bad: 函数超过 200 行
|
|
1858
|
+
|
|
1859
|
+
\`\`\`javascript
|
|
1860
|
+
function getUserInfo() {
|
|
1861
|
+
// ... 大于 200
|
|
1862
|
+
}
|
|
1863
|
+
\`\`\``;
|
|
1864
|
+
|
|
1865
|
+
const spec = service.parseSpecFile("js&ts.base.md", mockContent);
|
|
1866
|
+
expect(spec).not.toBeNull();
|
|
1867
|
+
expect(spec!.rules[1].examples).toHaveLength(1);
|
|
1868
|
+
expect(spec!.rules[1].examples[0].title).toBe("函数行数");
|
|
1869
|
+
expect(spec!.rules[1].examples[0].description).toBe("例子说明 desc");
|
|
1870
|
+
expect(spec!.rules[1].examples[0].content).toHaveLength(2);
|
|
1871
|
+
expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
|
|
1872
|
+
expect(spec!.rules[1].examples[0].content[0].title).toBe("函数不超过 200 行");
|
|
1873
|
+
});
|
|
1874
|
+
});
|
|
1620
1875
|
});
|
|
@@ -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
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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(
|
|
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,69 @@ 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
|
-
for (const section of sections) {
|
|
690
|
-
const trimmedSection = section.trim();
|
|
691
|
-
if (!trimmedSection) continue;
|
|
692
689
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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;
|
|
696
|
+
|
|
697
|
+
// 提取分组标题和描述,如 "### Example: 函数行数"
|
|
698
|
+
let exampleTitle = "";
|
|
699
|
+
let exampleDescription = "";
|
|
700
|
+
const groupMatch = trimmedGroup.match(/^###\s+Example\s*[::]\s*(.+)/i);
|
|
701
|
+
if (groupMatch) {
|
|
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
|
+
}
|
|
698
709
|
}
|
|
699
710
|
|
|
700
|
-
|
|
711
|
+
// 在分组内按 #### 提取 Good/Bad
|
|
712
|
+
const ruleContents: RuleContent[] = [];
|
|
713
|
+
const sections = trimmedGroup.split(/(?:^|\n)####\s+/);
|
|
714
|
+
|
|
715
|
+
for (const section of sections) {
|
|
716
|
+
const trimmedSection = section.trim();
|
|
717
|
+
if (!trimmedSection) continue;
|
|
718
|
+
|
|
719
|
+
let type: "good" | "bad" | null = null;
|
|
720
|
+
let contentTitle = "";
|
|
721
|
+
if (/^good:/i.test(trimmedSection)) {
|
|
722
|
+
type = "good";
|
|
723
|
+
contentTitle = trimmedSection.match(/^good:\s*(.+)/i)?.[1]?.trim() ?? "";
|
|
724
|
+
} else if (/^bad:/i.test(trimmedSection)) {
|
|
725
|
+
type = "bad";
|
|
726
|
+
contentTitle = trimmedSection.match(/^bad:\s*(.+)/i)?.[1]?.trim() ?? "";
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!type) continue;
|
|
701
730
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
731
|
+
// 提取代码块作为 description
|
|
732
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
733
|
+
let codeMatch;
|
|
734
|
+
const codeParts: string[] = [];
|
|
735
|
+
while ((codeMatch = codeBlockRegex.exec(trimmedSection)) !== null) {
|
|
736
|
+
codeParts.push(codeMatch[2].trim());
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
ruleContents.push({
|
|
740
|
+
title: contentTitle,
|
|
741
|
+
type,
|
|
742
|
+
description: codeParts.join("\n\n"),
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (ruleContents.length > 0) {
|
|
747
|
+
examples.push({
|
|
748
|
+
title: exampleTitle,
|
|
749
|
+
description: exampleDescription,
|
|
750
|
+
content: ruleContents,
|
|
751
|
+
});
|
|
708
752
|
}
|
|
709
753
|
}
|
|
710
754
|
|
|
@@ -748,10 +792,12 @@ export class ReviewSpecService {
|
|
|
748
792
|
* 根据 spec 的 includes 配置过滤 issues
|
|
749
793
|
* 只保留文件名匹配对应 spec includes 模式的 issues
|
|
750
794
|
* 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
|
|
795
|
+
* 支持 `added|`/`modified|`/`deleted|` 前缀语法
|
|
751
796
|
*/
|
|
752
797
|
filterIssuesByIncludes<T extends { file: string; ruleId: string }>(
|
|
753
798
|
issues: T[],
|
|
754
799
|
specs: ReviewSpec[],
|
|
800
|
+
changedFiles?: ChangedFileCollection,
|
|
755
801
|
): T[] {
|
|
756
802
|
// 构建 spec filename -> includes 的映射
|
|
757
803
|
const specIncludesMap = new Map<string, string[]>();
|
|
@@ -771,14 +817,9 @@ export class ReviewSpecService {
|
|
|
771
817
|
return true;
|
|
772
818
|
}
|
|
773
819
|
|
|
774
|
-
// 检查文件是否匹配 includes
|
|
775
|
-
const
|
|
776
|
-
|
|
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;
|
|
820
|
+
// 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
|
|
821
|
+
const fileStatus = changedFiles?.getStatus(issue.file);
|
|
822
|
+
return matchIncludes(includes, issue.file, fileStatus);
|
|
782
823
|
});
|
|
783
824
|
}
|
|
784
825
|
|
|
@@ -816,6 +857,7 @@ export class ReviewSpecService {
|
|
|
816
857
|
filterIssuesByOverrides<T extends { ruleId: string; file?: string }>(
|
|
817
858
|
issues: T[],
|
|
818
859
|
specs: ReviewSpec[],
|
|
860
|
+
changedFiles?: ChangedFileCollection,
|
|
819
861
|
verbose?: VerboseLevel,
|
|
820
862
|
): T[] {
|
|
821
863
|
// ========== 阶段1: 收集 spec -> overrides 的映射(保留作用域信息) ==========
|
|
@@ -887,10 +929,9 @@ export class ReviewSpecService {
|
|
|
887
929
|
if (scoped.includes.length === 0) {
|
|
888
930
|
return true;
|
|
889
931
|
}
|
|
890
|
-
// 使用
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
return issueFile && micromatch.isMatch(issueFile, globs, { matchBase: true });
|
|
932
|
+
// 使用 matchIncludes 检查文件是否匹配 includes 模式(支持 status|glob 前缀)
|
|
933
|
+
const fileStatus = changedFiles?.getStatus(issueFile);
|
|
934
|
+
return matchIncludes(scoped.includes, issueFile, fileStatus);
|
|
894
935
|
});
|
|
895
936
|
|
|
896
937
|
if (matched) {
|
|
@@ -1011,29 +1052,7 @@ export class ReviewSpecService {
|
|
|
1011
1052
|
* 构建 specs 的 prompt 部分
|
|
1012
1053
|
*/
|
|
1013
1054
|
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");
|
|
1055
|
+
return buildSpecsSectionPrompt(specs);
|
|
1037
1056
|
}
|
|
1038
1057
|
|
|
1039
1058
|
/**
|
package/src/review-spec/types.ts
CHANGED
|
@@ -24,9 +24,15 @@ export interface ReviewSpec {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface RuleExample {
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
package/src/review.service.ts
CHANGED
|
@@ -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(
|
|
293
|
+
filtered = this.reviewSpecService.filterIssuesByOverrides(
|
|
294
|
+
filtered,
|
|
295
|
+
specs,
|
|
296
|
+
changedFiles,
|
|
297
|
+
verbose,
|
|
298
|
+
);
|
|
295
299
|
|
|
296
300
|
// 变更行过滤
|
|
297
301
|
if (shouldLog(verbose, 3)) {
|