@spaceflow/review 0.83.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.js +209 -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 +229 -11
- package/src/review-spec/review-spec.service.ts +69 -55
- package/src/review-spec/types.ts +8 -2
- package/src/review.service.ts +7 -3
- package/dist/551.js +0 -9
|
@@ -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,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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
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;
|
|
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
|
-
// 使用
|
|
891
|
-
const
|
|
892
|
-
|
|
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
|
/**
|
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)) {
|