@spaceflow/review 0.29.2 → 0.30.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 +26 -0
- package/dist/index.js +4488 -5043
- package/package.json +3 -10
- package/src/deletion-impact.service.spec.ts +8 -19
- package/src/deletion-impact.service.ts +0 -2
- package/src/index.ts +118 -24
- package/src/issue-verify.service.spec.ts +7 -23
- package/src/issue-verify.service.ts +0 -2
- package/src/mcp/index.ts +191 -0
- package/src/review-report/index.ts +0 -1
- package/src/review-report/review-report.service.ts +0 -2
- package/src/review-spec/index.ts +0 -1
- package/src/review-spec/review-spec.service.spec.ts +3 -8
- package/src/review-spec/review-spec.service.ts +1 -4
- package/src/review.config.ts +59 -21
- package/src/review.service.spec.ts +33 -81
- package/src/review.service.ts +4 -6
- package/tsconfig.json +1 -1
- package/src/dto/mcp.dto.ts +0 -42
- package/src/review-report/review-report.module.ts +0 -8
- package/src/review-spec/review-spec.module.ts +0 -10
- package/src/review.command.ts +0 -244
- package/src/review.mcp.ts +0 -184
- package/src/review.module.ts +0 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spaceflow/review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lydanne",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"micromatch": "^4.0.8"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@nestjs/testing": "^11.0.1",
|
|
22
21
|
"@swc/core": "1.15.3",
|
|
23
22
|
"@types/micromatch": "^4.0.9",
|
|
24
23
|
"@types/node": "^22.15.0",
|
|
@@ -26,16 +25,10 @@
|
|
|
26
25
|
"@vitest/coverage-v8": "^4.0.18",
|
|
27
26
|
"unplugin-swc": "^1.5.9",
|
|
28
27
|
"vitest": "^4.0.18",
|
|
29
|
-
"@spaceflow/cli": "0.
|
|
28
|
+
"@spaceflow/cli": "0.20.0"
|
|
30
29
|
},
|
|
31
30
|
"peerDependencies": {
|
|
32
|
-
"@
|
|
33
|
-
"@nestjs/config": "^4.0.2",
|
|
34
|
-
"@nestjs/swagger": "^11.2.6",
|
|
35
|
-
"class-transformer": "^0.5.1",
|
|
36
|
-
"class-validator": "^0.14.3",
|
|
37
|
-
"nest-commander": "^3.20.1",
|
|
38
|
-
"@spaceflow/core": "0.1.3"
|
|
31
|
+
"@spaceflow/core": "0.2.0"
|
|
39
32
|
},
|
|
40
33
|
"spaceflow": {
|
|
41
34
|
"type": "flow",
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { vi, type
|
|
2
|
-
import { Test, TestingModule } from "@nestjs/testing";
|
|
3
|
-
import { LlmProxyService, GitProviderService } from "@spaceflow/core";
|
|
1
|
+
import { vi, type Mock } from "vitest";
|
|
4
2
|
import { DeletionImpactService } from "./deletion-impact.service";
|
|
5
3
|
import * as child_process from "child_process";
|
|
6
4
|
import { EventEmitter } from "events";
|
|
@@ -13,30 +11,21 @@ vi.mock("child_process");
|
|
|
13
11
|
|
|
14
12
|
describe("DeletionImpactService", () => {
|
|
15
13
|
let service: DeletionImpactService;
|
|
16
|
-
let llmProxyService:
|
|
17
|
-
let gitProvider:
|
|
14
|
+
let llmProxyService: any;
|
|
15
|
+
let gitProvider: any;
|
|
18
16
|
|
|
19
|
-
beforeEach(
|
|
20
|
-
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
llmProxyService = {
|
|
21
20
|
chatStream: vi.fn(),
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
gitProvider = {
|
|
25
24
|
getPullRequestFiles: vi.fn(),
|
|
26
25
|
getPullRequestDiff: vi.fn(),
|
|
27
26
|
};
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
providers: [
|
|
31
|
-
DeletionImpactService,
|
|
32
|
-
{ provide: LlmProxyService, useValue: mockLlmProxyService },
|
|
33
|
-
{ provide: GitProviderService, useValue: mockGitProvider },
|
|
34
|
-
],
|
|
35
|
-
}).compile();
|
|
36
|
-
|
|
37
|
-
service = module.get<DeletionImpactService>(DeletionImpactService);
|
|
38
|
-
llmProxyService = module.get(LlmProxyService) as Mocked<LlmProxyService>;
|
|
39
|
-
gitProvider = module.get(GitProviderService) as Mocked<GitProviderService>;
|
|
28
|
+
service = new DeletionImpactService(llmProxyService as any, gitProvider as any);
|
|
40
29
|
});
|
|
41
30
|
|
|
42
31
|
describe("analyzeDeletionImpact", () => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Injectable,
|
|
3
2
|
LlmProxyService,
|
|
4
3
|
logStreamEvent,
|
|
5
4
|
createStreamLoggerState,
|
|
@@ -73,7 +72,6 @@ export interface DeletionAnalysisContext {
|
|
|
73
72
|
includes?: string[];
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
@Injectable()
|
|
77
75
|
export class DeletionImpactService {
|
|
78
76
|
constructor(
|
|
79
77
|
protected readonly llmProxyService: LlmProxyService,
|
package/src/index.ts
CHANGED
|
@@ -1,32 +1,126 @@
|
|
|
1
1
|
import "./locales";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { defineExtension, t } from "@spaceflow/core";
|
|
3
|
+
import type {
|
|
4
|
+
GitProviderService,
|
|
5
|
+
ConfigReaderService,
|
|
6
|
+
LlmProxyService,
|
|
7
|
+
GitSdkService,
|
|
8
|
+
LLMMode,
|
|
9
|
+
} from "@spaceflow/core";
|
|
10
|
+
import { parseVerbose } from "@spaceflow/core";
|
|
11
|
+
import { reviewSchema, type AnalyzeDeletionsMode } from "./review.config";
|
|
12
|
+
import { ReviewService } from "./review.service";
|
|
13
|
+
import { ReviewSpecService } from "./review-spec";
|
|
14
|
+
import { ReviewReportService, type ReportFormat } from "./review-report";
|
|
15
|
+
import { IssueVerifyService } from "./issue-verify.service";
|
|
16
|
+
import { DeletionImpactService } from "./deletion-impact.service";
|
|
17
|
+
import { reviewMcpServer } from "./mcp";
|
|
18
|
+
|
|
19
|
+
export const extension = defineExtension({
|
|
7
20
|
name: "review",
|
|
8
|
-
commands: ["review"],
|
|
9
|
-
configKey: "review",
|
|
10
|
-
configSchema: reviewSchema,
|
|
11
21
|
version: "1.0.0",
|
|
12
22
|
description: t("review:extensionDescription"),
|
|
13
|
-
|
|
23
|
+
configKey: "review",
|
|
24
|
+
configSchema: reviewSchema,
|
|
25
|
+
mcp: reviewMcpServer,
|
|
26
|
+
commands: [
|
|
27
|
+
{
|
|
28
|
+
name: "review",
|
|
29
|
+
description: t("review:description"),
|
|
30
|
+
options: [
|
|
31
|
+
{ flags: "-d, --dry-run", description: t("review:options.dryRun") },
|
|
32
|
+
{ flags: "-c, --ci", description: t("common.options.ci") },
|
|
33
|
+
{ flags: "-p, --pr-number <number>", description: t("review:options.prNumber") },
|
|
34
|
+
{ flags: "-b, --base <ref>", description: t("review:options.base") },
|
|
35
|
+
{ flags: "--head <ref>", description: t("review:options.head") },
|
|
36
|
+
{ flags: "-i, --includes <patterns...>", description: t("review:options.includes") },
|
|
37
|
+
{ flags: "-l, --llm-mode <mode>", description: t("review:options.llmMode") },
|
|
38
|
+
{ flags: "-f, --files <files...>", description: t("review:options.files") },
|
|
39
|
+
{ flags: "--commits <commits...>", description: t("review:options.commits") },
|
|
40
|
+
{ flags: "--verify-fixes", description: t("review:options.verifyFixes") },
|
|
41
|
+
{ flags: "--no-verify-fixes", description: t("review:options.noVerifyFixes") },
|
|
42
|
+
{ flags: "--analyze-deletions [mode]", description: t("review:options.analyzeDeletions") },
|
|
43
|
+
{
|
|
44
|
+
flags: "--deletion-analysis-mode <mode>",
|
|
45
|
+
description: t("review:options.deletionAnalysisMode"),
|
|
46
|
+
},
|
|
47
|
+
{ flags: "--deletion-only", description: t("review:options.deletionOnly") },
|
|
48
|
+
{ flags: "-o, --output-format <format>", description: t("review:options.outputFormat") },
|
|
49
|
+
{ flags: "--generate-description", description: t("review:options.generateDescription") },
|
|
50
|
+
{ flags: "--show-all", description: t("review:options.showAll") },
|
|
51
|
+
{ flags: "--event-action <action>", description: t("review:options.eventAction") },
|
|
52
|
+
],
|
|
53
|
+
run: async (_args, options, ctx) => {
|
|
54
|
+
if (!ctx.hasService("gitProvider")) {
|
|
55
|
+
ctx.output.error(
|
|
56
|
+
"review 命令需要配置 Git Provider,请在 spaceflow.json 中配置 gitProvider 字段",
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (!ctx.hasService("llmProxy")) {
|
|
61
|
+
ctx.output.error("review 命令需要配置 LLM 服务,请在 spaceflow.json 中配置 llm 字段");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const gitProvider = ctx.getService<GitProviderService>("gitProvider");
|
|
66
|
+
const configReader = ctx.getService<ConfigReaderService>("config");
|
|
67
|
+
const llmProxy = ctx.getService<LlmProxyService>("llmProxy");
|
|
68
|
+
const gitSdk = ctx.hasService("gitSdk")
|
|
69
|
+
? ctx.getService<GitSdkService>("gitSdk")
|
|
70
|
+
: undefined;
|
|
71
|
+
|
|
72
|
+
const reviewSpecService = new ReviewSpecService(gitProvider);
|
|
73
|
+
const reviewReportService = new ReviewReportService();
|
|
74
|
+
const issueVerifyService = new IssueVerifyService(llmProxy, reviewSpecService);
|
|
75
|
+
const deletionImpactService = new DeletionImpactService(llmProxy, gitProvider);
|
|
14
76
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
77
|
+
const reviewService = new ReviewService(
|
|
78
|
+
gitProvider,
|
|
79
|
+
ctx.config,
|
|
80
|
+
configReader,
|
|
81
|
+
reviewSpecService,
|
|
82
|
+
llmProxy,
|
|
83
|
+
reviewReportService,
|
|
84
|
+
issueVerifyService,
|
|
85
|
+
deletionImpactService,
|
|
86
|
+
gitSdk!,
|
|
87
|
+
);
|
|
19
88
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
89
|
+
const reviewOptions = {
|
|
90
|
+
dryRun: !!options?.dryRun,
|
|
91
|
+
ci: !!options?.ci,
|
|
92
|
+
prNumber: options?.prNumber ? parseInt(options.prNumber as string, 10) : undefined,
|
|
93
|
+
base: options?.base as string,
|
|
94
|
+
head: options?.head as string,
|
|
95
|
+
verbose: parseVerbose(options?.verbose as string | boolean | undefined),
|
|
96
|
+
includes: options?.includes as string[],
|
|
97
|
+
llmMode: options?.llmMode as LLMMode,
|
|
98
|
+
files: options?.files as string[],
|
|
99
|
+
commits: options?.commits as string[],
|
|
100
|
+
verifyFixes: options?.verifyFixes as boolean,
|
|
101
|
+
analyzeDeletions: options?.analyzeDeletions as AnalyzeDeletionsMode,
|
|
102
|
+
deletionAnalysisMode: options?.deletionAnalysisMode as LLMMode,
|
|
103
|
+
deletionOnly: !!options?.deletionOnly,
|
|
104
|
+
outputFormat: options?.outputFormat as ReportFormat,
|
|
105
|
+
generateDescription: !!options?.generateDescription,
|
|
106
|
+
showAll: !!options?.showAll,
|
|
107
|
+
eventAction: options?.eventAction as string,
|
|
108
|
+
};
|
|
24
109
|
|
|
25
|
-
|
|
110
|
+
try {
|
|
111
|
+
const context = await reviewService.getContextFromEnv(reviewOptions);
|
|
112
|
+
await reviewService.execute(context);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof Error) {
|
|
115
|
+
ctx.output.error(t("common.executionFailed", { error: error.message }));
|
|
116
|
+
} else {
|
|
117
|
+
ctx.output.error(t("common.executionFailed", { error: String(error) }));
|
|
118
|
+
}
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
26
125
|
|
|
27
|
-
export
|
|
28
|
-
export * from "./review.command";
|
|
29
|
-
export * from "./review.service";
|
|
30
|
-
export * from "./review.mcp";
|
|
31
|
-
export * from "./issue-verify.service";
|
|
32
|
-
export * from "./deletion-impact.service";
|
|
126
|
+
export default extension;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { vi
|
|
2
|
-
import {
|
|
3
|
-
import { LlmProxyService } from "@spaceflow/core";
|
|
4
|
-
import { ReviewIssue, FileContentsMap, ReviewSpecService } from "./review-spec";
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import { ReviewIssue, FileContentsMap } from "./review-spec";
|
|
5
3
|
import { IssueVerifyService } from "./issue-verify.service";
|
|
6
4
|
|
|
7
5
|
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
@@ -10,10 +8,11 @@ vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
|
10
8
|
|
|
11
9
|
describe("IssueVerifyService", () => {
|
|
12
10
|
let service: IssueVerifyService;
|
|
13
|
-
let llmProxyService:
|
|
11
|
+
let llmProxyService: any;
|
|
14
12
|
|
|
15
|
-
beforeEach(
|
|
16
|
-
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
llmProxyService = {
|
|
17
16
|
chatStream: vi.fn(),
|
|
18
17
|
};
|
|
19
18
|
|
|
@@ -22,22 +21,7 @@ describe("IssueVerifyService", () => {
|
|
|
22
21
|
buildSpecsSection: vi.fn().mockReturnValue("mock rule specs"),
|
|
23
22
|
};
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
providers: [
|
|
27
|
-
IssueVerifyService,
|
|
28
|
-
{
|
|
29
|
-
provide: LlmProxyService,
|
|
30
|
-
useValue: mockLlmProxyService,
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
provide: ReviewSpecService,
|
|
34
|
-
useValue: mockReviewSpecService,
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
}).compile();
|
|
38
|
-
|
|
39
|
-
service = module.get<IssueVerifyService>(IssueVerifyService);
|
|
40
|
-
llmProxyService = module.get(LlmProxyService) as Mocked<LlmProxyService>;
|
|
24
|
+
service = new IssueVerifyService(llmProxyService as any, mockReviewSpecService as any);
|
|
41
25
|
});
|
|
42
26
|
|
|
43
27
|
it("should return empty array if no issues provided", async () => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Injectable,
|
|
3
2
|
LlmProxyService,
|
|
4
3
|
type LLMMode,
|
|
5
4
|
type VerboseLevel,
|
|
@@ -46,7 +45,6 @@ const VERIFY_SCHEMA: LlmJsonPutSchema = {
|
|
|
46
45
|
additionalProperties: false,
|
|
47
46
|
};
|
|
48
47
|
|
|
49
|
-
@Injectable()
|
|
50
48
|
export class IssueVerifyService {
|
|
51
49
|
constructor(
|
|
52
50
|
protected readonly llmProxyService: LlmProxyService,
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
t,
|
|
3
|
+
z,
|
|
4
|
+
type McpServerDefinition,
|
|
5
|
+
type SpaceflowContext,
|
|
6
|
+
type GitProviderService,
|
|
7
|
+
} from "@spaceflow/core";
|
|
8
|
+
import { ReviewSpecService } from "../review-spec";
|
|
9
|
+
import type { ReviewConfig } from "../review.config";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
|
|
13
|
+
/** MCP 工具输入 schema */
|
|
14
|
+
export const listRulesInputSchema = z.object({
|
|
15
|
+
cwd: z.string().optional().describe(t("review:mcp.dto.cwd")),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const getRulesForFileInputSchema = z.object({
|
|
19
|
+
filePath: z.string().describe(t("review:mcp.dto.filePath")),
|
|
20
|
+
cwd: z.string().optional().describe(t("review:mcp.dto.cwd")),
|
|
21
|
+
includeExamples: z.boolean().optional().describe(t("review:mcp.dto.includeExamples")),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const getRuleDetailInputSchema = z.object({
|
|
25
|
+
ruleId: z.string().describe(t("review:mcp.dto.ruleId")),
|
|
26
|
+
cwd: z.string().optional().describe(t("review:mcp.dto.cwd")),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 获取 GitProviderService(可选)
|
|
31
|
+
*/
|
|
32
|
+
function getGitProvider(ctx: SpaceflowContext): GitProviderService | undefined {
|
|
33
|
+
try {
|
|
34
|
+
return ctx.getService<GitProviderService>("gitProvider");
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 获取项目的规则目录
|
|
42
|
+
*/
|
|
43
|
+
async function getSpecDirs(cwd: string, ctx: SpaceflowContext): Promise<string[]> {
|
|
44
|
+
const dirs: string[] = [];
|
|
45
|
+
try {
|
|
46
|
+
const reviewConfig = ctx.config.get<ReviewConfig>("review");
|
|
47
|
+
if (reviewConfig?.references?.length) {
|
|
48
|
+
const gitProvider = getGitProvider(ctx);
|
|
49
|
+
const specService = new ReviewSpecService(gitProvider);
|
|
50
|
+
const resolved = await specService.resolveSpecSources(reviewConfig.references);
|
|
51
|
+
dirs.push(...resolved);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// 忽略配置读取错误
|
|
55
|
+
}
|
|
56
|
+
const defaultDirs = [
|
|
57
|
+
join(cwd, ".claude", "skills"),
|
|
58
|
+
join(cwd, ".cursor", "skills"),
|
|
59
|
+
join(cwd, "review-specs"),
|
|
60
|
+
];
|
|
61
|
+
for (const dir of defaultDirs) {
|
|
62
|
+
if (existsSync(dir)) {
|
|
63
|
+
dirs.push(dir);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [...new Set(dirs)];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 加载所有规则
|
|
71
|
+
*/
|
|
72
|
+
async function loadAllSpecs(cwd: string, ctx: SpaceflowContext) {
|
|
73
|
+
const gitProvider = getGitProvider(ctx);
|
|
74
|
+
const specService = new ReviewSpecService(gitProvider);
|
|
75
|
+
const specDirs = await getSpecDirs(cwd, ctx);
|
|
76
|
+
const allSpecs = [];
|
|
77
|
+
for (const dir of specDirs) {
|
|
78
|
+
const specs = await specService.loadReviewSpecs(dir);
|
|
79
|
+
allSpecs.push(...specs);
|
|
80
|
+
}
|
|
81
|
+
return specService.deduplicateSpecs(allSpecs);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Review MCP 服务器定义
|
|
86
|
+
*/
|
|
87
|
+
export const reviewMcpServer: McpServerDefinition = {
|
|
88
|
+
name: "review-mcp",
|
|
89
|
+
version: "1.0.0",
|
|
90
|
+
description: t("review:mcp.serverDescription"),
|
|
91
|
+
tools: [
|
|
92
|
+
{
|
|
93
|
+
name: "list_rules",
|
|
94
|
+
description: t("review:mcp.listRules"),
|
|
95
|
+
inputSchema: listRulesInputSchema,
|
|
96
|
+
handler: async (input, ctx) => {
|
|
97
|
+
const { cwd } = input as z.infer<typeof listRulesInputSchema>;
|
|
98
|
+
const workDir = cwd || process.cwd();
|
|
99
|
+
const specs = await loadAllSpecs(workDir, ctx);
|
|
100
|
+
const rules = specs.flatMap((spec) =>
|
|
101
|
+
spec.rules.map((rule) => ({
|
|
102
|
+
id: rule.id,
|
|
103
|
+
title: rule.title,
|
|
104
|
+
description:
|
|
105
|
+
rule.description.slice(0, 200) + (rule.description.length > 200 ? "..." : ""),
|
|
106
|
+
severity: rule.severity || spec.severity,
|
|
107
|
+
extensions: spec.extensions,
|
|
108
|
+
specFile: spec.filename,
|
|
109
|
+
includes: spec.includes,
|
|
110
|
+
hasExamples: rule.examples.length > 0,
|
|
111
|
+
})),
|
|
112
|
+
);
|
|
113
|
+
return { total: rules.length, rules };
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "get_rules_for_file",
|
|
118
|
+
description: t("review:mcp.getRulesForFile"),
|
|
119
|
+
inputSchema: getRulesForFileInputSchema,
|
|
120
|
+
handler: async (input, ctx) => {
|
|
121
|
+
const { filePath, cwd, includeExamples } = input as z.infer<
|
|
122
|
+
typeof getRulesForFileInputSchema
|
|
123
|
+
>;
|
|
124
|
+
const workDir = cwd || process.cwd();
|
|
125
|
+
const allSpecs = await loadAllSpecs(workDir, ctx);
|
|
126
|
+
const specService = new ReviewSpecService();
|
|
127
|
+
const applicableSpecs = specService.filterApplicableSpecs(allSpecs, [
|
|
128
|
+
{ filename: filePath },
|
|
129
|
+
]);
|
|
130
|
+
const micromatchModule = await import("micromatch");
|
|
131
|
+
const micromatch = micromatchModule.default || micromatchModule;
|
|
132
|
+
const rules = applicableSpecs.flatMap((spec) =>
|
|
133
|
+
spec.rules
|
|
134
|
+
.filter((rule) => {
|
|
135
|
+
const includes = rule.includes || spec.includes;
|
|
136
|
+
if (includes.length === 0) return true;
|
|
137
|
+
return micromatch.isMatch(filePath, includes, { matchBase: true });
|
|
138
|
+
})
|
|
139
|
+
.map((rule) => ({
|
|
140
|
+
id: rule.id,
|
|
141
|
+
title: rule.title,
|
|
142
|
+
description: rule.description,
|
|
143
|
+
severity: rule.severity || spec.severity,
|
|
144
|
+
specFile: spec.filename,
|
|
145
|
+
...(includeExamples && rule.examples.length > 0
|
|
146
|
+
? {
|
|
147
|
+
examples: rule.examples.map((ex) => ({
|
|
148
|
+
type: ex.type,
|
|
149
|
+
lang: ex.lang,
|
|
150
|
+
code: ex.code,
|
|
151
|
+
})),
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
})),
|
|
155
|
+
);
|
|
156
|
+
return { file: filePath, total: rules.length, rules };
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "get_rule_detail",
|
|
161
|
+
description: t("review:mcp.getRuleDetail"),
|
|
162
|
+
inputSchema: getRuleDetailInputSchema,
|
|
163
|
+
handler: async (input, ctx) => {
|
|
164
|
+
const { ruleId, cwd } = input as z.infer<typeof getRuleDetailInputSchema>;
|
|
165
|
+
const workDir = cwd || process.cwd();
|
|
166
|
+
const specs = await loadAllSpecs(workDir, ctx);
|
|
167
|
+
const specService = new ReviewSpecService();
|
|
168
|
+
const result = specService.findRuleById(ruleId, specs);
|
|
169
|
+
if (!result) {
|
|
170
|
+
return { error: t("review:mcp.ruleNotFound", { ruleId }) };
|
|
171
|
+
}
|
|
172
|
+
const { rule, spec } = result;
|
|
173
|
+
return {
|
|
174
|
+
id: rule.id,
|
|
175
|
+
title: rule.title,
|
|
176
|
+
description: rule.description,
|
|
177
|
+
severity: rule.severity || spec.severity,
|
|
178
|
+
specFile: spec.filename,
|
|
179
|
+
extensions: spec.extensions,
|
|
180
|
+
includes: spec.includes,
|
|
181
|
+
overrides: rule.overrides,
|
|
182
|
+
examples: rule.examples.map((ex) => ({
|
|
183
|
+
type: ex.type,
|
|
184
|
+
lang: ex.lang,
|
|
185
|
+
code: ex.code,
|
|
186
|
+
})),
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { Injectable } from "@nestjs/common";
|
|
2
1
|
import { ReviewResult, ReviewStats } from "../review-spec/types";
|
|
3
2
|
import { JsonFormatter, MarkdownFormatter, TerminalFormatter } from "./formatters";
|
|
4
3
|
import { ParsedReport, ReportFormat, ReportOptions, ReviewReportFormatter } from "./types";
|
|
5
4
|
|
|
6
|
-
@Injectable()
|
|
7
5
|
export class ReviewReportService {
|
|
8
6
|
private readonly markdownFormatter = new MarkdownFormatter();
|
|
9
7
|
private readonly terminalFormatter = new TerminalFormatter();
|
package/src/review-spec/index.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { vi, type Mock } from "vitest";
|
|
2
|
-
import { Test, TestingModule } from "@nestjs/testing";
|
|
3
2
|
import { ReviewSpecService } from "./review-spec.service";
|
|
4
|
-
import { GitProviderService } from "@spaceflow/core";
|
|
5
3
|
import { readdir, readFile, mkdir, access, writeFile } from "fs/promises";
|
|
6
4
|
import * as child_process from "child_process";
|
|
7
5
|
|
|
@@ -13,16 +11,13 @@ describe("ReviewSpecService", () => {
|
|
|
13
11
|
|
|
14
12
|
let gitProvider: { listRepositoryContents: Mock; getFileContent: Mock };
|
|
15
13
|
|
|
16
|
-
beforeEach(
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
17
16
|
gitProvider = {
|
|
18
17
|
listRepositoryContents: vi.fn(),
|
|
19
18
|
getFileContent: vi.fn(),
|
|
20
19
|
};
|
|
21
|
-
|
|
22
|
-
providers: [ReviewSpecService, { provide: GitProviderService, useValue: gitProvider }],
|
|
23
|
-
}).compile();
|
|
24
|
-
|
|
25
|
-
service = module.get<ReviewSpecService>(ReviewSpecService);
|
|
20
|
+
service = new ReviewSpecService(gitProvider as any);
|
|
26
21
|
});
|
|
27
22
|
|
|
28
23
|
afterEach(() => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Injectable,
|
|
3
2
|
type ChangedFile,
|
|
4
3
|
type VerboseLevel,
|
|
5
4
|
shouldLog,
|
|
@@ -8,7 +7,6 @@ import {
|
|
|
8
7
|
type RemoteRepoRef,
|
|
9
8
|
type RepositoryContent,
|
|
10
9
|
} from "@spaceflow/core";
|
|
11
|
-
import { Optional } from "@nestjs/common";
|
|
12
10
|
import { readdir, readFile, mkdir, access, writeFile } from "fs/promises";
|
|
13
11
|
import { join, basename, extname } from "path";
|
|
14
12
|
import { homedir } from "os";
|
|
@@ -19,9 +17,8 @@ import { ReviewSpec, ReviewRule, RuleExample, Severity } from "./types";
|
|
|
19
17
|
/** 远程规则缓存 TTL(毫秒),默认 5 分钟 */
|
|
20
18
|
const REMOTE_SPEC_CACHE_TTL = 5 * 60 * 1000;
|
|
21
19
|
|
|
22
|
-
@Injectable()
|
|
23
20
|
export class ReviewSpecService {
|
|
24
|
-
constructor(
|
|
21
|
+
constructor(protected readonly gitProvider?: GitProviderService) {}
|
|
25
22
|
/**
|
|
26
23
|
* 检查规则 ID 是否匹配(精确匹配或前缀匹配)
|
|
27
24
|
* 例如: "JsTs.FileName" 匹配 "JsTs.FileName" 和 "JsTs.FileName.UpperCamel"
|
package/src/review.config.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "@spaceflow/core";
|
|
2
|
+
import type { LLMMode, VerboseLevel } from "@spaceflow/core";
|
|
3
|
+
import type { ReportFormat } from "./review-report";
|
|
2
4
|
|
|
3
5
|
/** LLM 模式 schema(与 core 中的 LLMMode 保持一致) */
|
|
4
6
|
const llmModeSchema = z.enum(["claude-code", "openai", "gemini", "open-code"]);
|
|
@@ -12,6 +14,63 @@ const severitySchema = z.enum(["off", "warn", "error"]);
|
|
|
12
14
|
/** 变更文件处理策略 schema */
|
|
13
15
|
const invalidateChangedFilesSchema = z.enum(["invalidate", "keep", "off"]);
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* 删除代码分析模式
|
|
19
|
+
* - true: 始终启用
|
|
20
|
+
* - false: 始终禁用
|
|
21
|
+
* - 'ci': 仅在 CI 环境中启用
|
|
22
|
+
* - 'pr': 仅在 PR 环境中启用
|
|
23
|
+
* - 'terminal': 仅在终端环境中启用
|
|
24
|
+
*/
|
|
25
|
+
export type AnalyzeDeletionsMode = z.infer<typeof analyzeDeletionsModeSchema>;
|
|
26
|
+
|
|
27
|
+
/** 审查规则严重级别 */
|
|
28
|
+
export type Severity = z.infer<typeof severitySchema>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 变更文件处理策略
|
|
32
|
+
* - 'invalidate': 将变更文件的历史问题标记为无效(默认)
|
|
33
|
+
* - 'keep': 保留历史问题,不做处理
|
|
34
|
+
* - 'off': 关闭此功能
|
|
35
|
+
*/
|
|
36
|
+
export type InvalidateChangedFilesMode = z.infer<typeof invalidateChangedFilesSchema>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Review 命令选项
|
|
40
|
+
*/
|
|
41
|
+
export interface ReviewOptions {
|
|
42
|
+
dryRun: boolean;
|
|
43
|
+
ci: boolean;
|
|
44
|
+
prNumber?: number;
|
|
45
|
+
base?: string;
|
|
46
|
+
head?: string;
|
|
47
|
+
references?: string[];
|
|
48
|
+
verbose?: VerboseLevel;
|
|
49
|
+
includes?: string[];
|
|
50
|
+
llmMode?: LLMMode;
|
|
51
|
+
files?: string[];
|
|
52
|
+
commits?: string[];
|
|
53
|
+
verifyFixes?: boolean;
|
|
54
|
+
verifyConcurrency?: number;
|
|
55
|
+
analyzeDeletions?: AnalyzeDeletionsMode;
|
|
56
|
+
/** 仅执行删除代码分析,跳过常规代码审查 */
|
|
57
|
+
deletionOnly?: boolean;
|
|
58
|
+
/** 删除代码分析模式:openai 使用标准模式,claude-agent 使用 Agent 模式 */
|
|
59
|
+
deletionAnalysisMode?: LLMMode;
|
|
60
|
+
/** 输出格式:markdown, terminal, json。不指定则智能选择 */
|
|
61
|
+
outputFormat?: ReportFormat;
|
|
62
|
+
/** 是否使用 AI 生成 PR 功能描述 */
|
|
63
|
+
generateDescription?: boolean;
|
|
64
|
+
/** 显示所有问题,不过滤非变更行的问题 */
|
|
65
|
+
showAll?: boolean;
|
|
66
|
+
/** PR 事件类型(opened, synchronize, closed 等) */
|
|
67
|
+
eventAction?: string;
|
|
68
|
+
concurrency?: number;
|
|
69
|
+
timeout?: number;
|
|
70
|
+
retries?: number;
|
|
71
|
+
retryDelay?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
15
74
|
/** review 命令配置 schema(LLM 敏感配置由系统 llm.config.ts 管理) */
|
|
16
75
|
export const reviewSchema = () =>
|
|
17
76
|
z.object({
|
|
@@ -33,26 +92,5 @@ export const reviewSchema = () =>
|
|
|
33
92
|
invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
|
|
34
93
|
});
|
|
35
94
|
|
|
36
|
-
/**
|
|
37
|
-
* 变更文件处理策略
|
|
38
|
-
* - 'invalidate': 将变更文件的历史问题标记为无效(默认)
|
|
39
|
-
* - 'keep': 保留历史问题,不做处理
|
|
40
|
-
* - 'off': 关闭此功能
|
|
41
|
-
*/
|
|
42
|
-
export type InvalidateChangedFilesMode = z.infer<typeof invalidateChangedFilesSchema>;
|
|
43
|
-
|
|
44
95
|
/** review 配置类型(从 schema 推导) */
|
|
45
96
|
export type ReviewConfig = z.infer<ReturnType<typeof reviewSchema>>;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 删除代码分析模式
|
|
49
|
-
* - true: 始终启用
|
|
50
|
-
* - false: 始终禁用
|
|
51
|
-
* - 'ci': 仅在 CI 环境中启用
|
|
52
|
-
* - 'pr': 仅在 PR 环境中启用
|
|
53
|
-
* - 'terminal': 仅在终端环境中启用
|
|
54
|
-
*/
|
|
55
|
-
export type AnalyzeDeletionsMode = z.infer<typeof analyzeDeletionsModeSchema>;
|
|
56
|
-
|
|
57
|
-
/** 审查规则严重级别 */
|
|
58
|
-
export type Severity = z.infer<typeof severitySchema>;
|