@spaceflow/review 0.29.3 → 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.
@@ -1,21 +1,8 @@
1
- import { vi, type Mocked, type Mock } from "vitest";
2
- import { Test, TestingModule } from "@nestjs/testing";
3
- import {
4
- ConfigService,
5
- ConfigReaderService,
6
- GitProviderService,
7
- ClaudeSetupService,
8
- LlmProxyService,
9
- GitSdkService,
10
- parseChangedLinesFromPatch,
11
- } from "@spaceflow/core";
12
- import { ReviewSpecService } from "./review-spec";
13
- import { ReviewReportService } from "./review-report";
1
+ import { vi, type Mock } from "vitest";
2
+ import { parseChangedLinesFromPatch } from "@spaceflow/core";
14
3
  import { readFile } from "fs/promises";
15
4
  import { ReviewService, ReviewContext, ReviewPrompt } from "./review.service";
16
- import { IssueVerifyService } from "./issue-verify.service";
17
- import { DeletionImpactService } from "./deletion-impact.service";
18
- import type { ReviewOptions } from "./review.command";
5
+ import type { ReviewOptions } from "./review.config";
19
6
 
20
7
  vi.mock("c12");
21
8
  vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
@@ -56,14 +43,17 @@ vi.mock("openai", () => {
56
43
 
57
44
  describe("ReviewService", () => {
58
45
  let service: ReviewService;
59
- let gitProvider: Mocked<GitProviderService>;
60
- let configService: Mocked<ConfigService>;
46
+ let gitProvider: any;
47
+ let configService: any;
61
48
  let mockReviewSpecService: any;
62
49
  let mockDeletionImpactService: any;
63
50
  let mockGitSdkService: any;
51
+ let mockLlmProxyService: any;
52
+ let mockConfigReaderService: any;
64
53
 
65
- beforeEach(async () => {
66
- const mockGitProvider = {
54
+ beforeEach(() => {
55
+ vi.clearAllMocks();
56
+ gitProvider = {
67
57
  validateConfig: vi.fn(),
68
58
  getPullRequest: vi.fn(),
69
59
  getCommit: vi.fn(),
@@ -80,14 +70,10 @@ describe("ReviewService", () => {
80
70
  getIssueCommentReactions: vi.fn().mockResolvedValue([]),
81
71
  };
82
72
 
83
- const mockConfigService = {
73
+ configService = {
84
74
  get: vi.fn(),
85
75
  };
86
76
 
87
- const mockClaudeSetupService = {
88
- configure: vi.fn(),
89
- };
90
-
91
77
  mockReviewSpecService = {
92
78
  resolveSpecSources: vi.fn().mockResolvedValue(["/mock/spec/dir"]),
93
79
  loadReviewSpecs: vi.fn().mockResolvedValue([
@@ -139,63 +125,29 @@ describe("ReviewService", () => {
139
125
  getCommitDiff: vi.fn().mockReturnValue([]),
140
126
  };
141
127
 
142
- const module: TestingModule = await Test.createTestingModule({
143
- providers: [
144
- ReviewService,
145
- {
146
- provide: GitProviderService,
147
- useValue: mockGitProvider,
148
- },
149
- {
150
- provide: ConfigService,
151
- useValue: mockConfigService,
152
- },
153
- {
154
- provide: ClaudeSetupService,
155
- useValue: mockClaudeSetupService,
156
- },
157
- {
158
- provide: ReviewSpecService,
159
- useValue: mockReviewSpecService,
160
- },
161
- {
162
- provide: LlmProxyService,
163
- useValue: {
164
- chat: vi.fn(),
165
- chatStream: vi.fn(),
166
- createSession: vi.fn(),
167
- getAvailableAdapters: vi.fn().mockReturnValue(["claude-code", "openai"]),
168
- },
169
- },
170
- {
171
- provide: ReviewReportService,
172
- useValue: mockReviewReportService,
173
- },
174
- {
175
- provide: IssueVerifyService,
176
- useValue: mockIssueVerifyService,
177
- },
178
- {
179
- provide: DeletionImpactService,
180
- useValue: mockDeletionImpactService,
181
- },
182
- {
183
- provide: GitSdkService,
184
- useValue: mockGitSdkService,
185
- },
186
- {
187
- provide: ConfigReaderService,
188
- useValue: {
189
- getPluginConfig: vi.fn().mockReturnValue({}),
190
- getSystemConfig: vi.fn().mockReturnValue({}),
191
- },
192
- },
193
- ],
194
- }).compile();
128
+ mockLlmProxyService = {
129
+ chat: vi.fn(),
130
+ chatStream: vi.fn(),
131
+ createSession: vi.fn(),
132
+ getAvailableAdapters: vi.fn().mockReturnValue(["claude-code", "openai"]),
133
+ };
134
+
135
+ mockConfigReaderService = {
136
+ getPluginConfig: vi.fn().mockReturnValue({}),
137
+ getSystemConfig: vi.fn().mockReturnValue({}),
138
+ };
195
139
 
196
- service = module.get<ReviewService>(ReviewService);
197
- gitProvider = module.get(GitProviderService) as Mocked<GitProviderService>;
198
- configService = module.get(ConfigService) as Mocked<ConfigService>;
140
+ service = new ReviewService(
141
+ gitProvider as any,
142
+ configService as any,
143
+ mockConfigReaderService as any,
144
+ mockReviewSpecService as any,
145
+ mockLlmProxyService as any,
146
+ mockReviewReportService as any,
147
+ mockIssueVerifyService as any,
148
+ mockDeletionImpactService as any,
149
+ mockGitSdkService as any,
150
+ );
199
151
  });
200
152
 
201
153
  afterEach(() => {
@@ -1,6 +1,4 @@
1
1
  import {
2
- Injectable,
3
- ConfigService,
4
2
  ConfigReaderService,
5
3
  GitProviderService,
6
4
  PullRequest,
@@ -24,6 +22,7 @@ import {
24
22
  parseHunksFromPatch,
25
23
  calculateNewLineNumber,
26
24
  } from "@spaceflow/core";
25
+ import type { IConfigReader } from "@spaceflow/core";
27
26
  import { type AnalyzeDeletionsMode, type ReviewConfig } from "./review.config";
28
27
  import {
29
28
  ReviewSpecService,
@@ -41,7 +40,7 @@ import { execSync } from "child_process";
41
40
  import { readFile, readdir } from "fs/promises";
42
41
  import { join, dirname, extname, relative, isAbsolute } from "path";
43
42
  import micromatch from "micromatch";
44
- import { ReviewOptions } from "./review.command";
43
+ import { ReviewOptions } from "./review.config";
45
44
  import { IssueVerifyService } from "./issue-verify.service";
46
45
  import { DeletionImpactService } from "./deletion-impact.service";
47
46
  import { parseTitleOptions } from "./parse-title-options";
@@ -138,13 +137,12 @@ const REVIEW_SCHEMA: LlmJsonPutSchema = {
138
137
  additionalProperties: false,
139
138
  };
140
139
 
141
- @Injectable()
142
140
  export class ReviewService {
143
141
  protected readonly llmJsonPut: LlmJsonPut<ReviewResult>;
144
142
 
145
143
  constructor(
146
144
  protected readonly gitProvider: GitProviderService,
147
- protected readonly configService: ConfigService,
145
+ protected readonly config: IConfigReader,
148
146
  protected readonly configReader: ConfigReaderService,
149
147
  protected readonly reviewSpecService: ReviewSpecService,
150
148
  protected readonly llmProxyService: LlmProxyService,
@@ -172,7 +170,7 @@ export class ReviewService {
172
170
 
173
171
  async getContextFromEnv(options: ReviewOptions): Promise<ReviewContext> {
174
172
  const reviewConf = this.configReader.getPluginConfig<ReviewConfig>("review");
175
- const ciConf = this.configService.get<CiConfig>("ci");
173
+ const ciConf = this.config.get<CiConfig>("ci");
176
174
  const repository = ciConf?.repository;
177
175
 
178
176
  if (options.ci) {
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../../core/tsconfig.skill.json",
2
+ "extends": "../../packages/core/tsconfig.skill.json",
3
3
  "compilerOptions": {
4
4
  "types": ["vitest/globals"]
5
5
  },
@@ -1,42 +0,0 @@
1
- import {
2
- ApiProperty,
3
- ApiPropertyOptional,
4
- IsString,
5
- IsBoolean,
6
- IsOptional,
7
- t,
8
- } from "@spaceflow/core";
9
-
10
- export class ListRulesInput {
11
- @ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
12
- @IsString()
13
- @IsOptional()
14
- cwd?: string;
15
- }
16
-
17
- export class GetRulesForFileInput {
18
- @ApiProperty({ description: t("review:mcp.dto.filePath") })
19
- @IsString()
20
- filePath!: string;
21
-
22
- @ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
23
- @IsString()
24
- @IsOptional()
25
- cwd?: string;
26
-
27
- @ApiPropertyOptional({ description: t("review:mcp.dto.includeExamples") })
28
- @IsBoolean()
29
- @IsOptional()
30
- includeExamples?: boolean;
31
- }
32
-
33
- export class GetRuleDetailInput {
34
- @ApiProperty({ description: t("review:mcp.dto.ruleId") })
35
- @IsString()
36
- ruleId!: string;
37
-
38
- @ApiPropertyOptional({ description: t("review:mcp.dto.cwd") })
39
- @IsString()
40
- @IsOptional()
41
- cwd?: string;
42
- }
@@ -1,8 +0,0 @@
1
- import { Module } from "@nestjs/common";
2
- import { ReviewReportService } from "./review-report.service";
3
-
4
- @Module({
5
- providers: [ReviewReportService],
6
- exports: [ReviewReportService],
7
- })
8
- export class ReviewReportModule {}
@@ -1,10 +0,0 @@
1
- import { Module } from "@nestjs/common";
2
- import { GitProviderModule } from "@spaceflow/core";
3
- import { ReviewSpecService } from "./review-spec.service";
4
-
5
- @Module({
6
- imports: [GitProviderModule.forFeature()],
7
- providers: [ReviewSpecService],
8
- exports: [ReviewSpecService],
9
- })
10
- export class ReviewSpecModule {}
@@ -1,244 +0,0 @@
1
- import { Command, CommandRunner, Option, t } from "@spaceflow/core";
2
- import type { LLMMode, VerboseLevel } from "@spaceflow/core";
3
- import type { AnalyzeDeletionsMode } from "./review.config";
4
- import type { ReportFormat } from "./review-report";
5
- import { ReviewService } from "./review.service";
6
-
7
- export interface ReviewOptions {
8
- dryRun: boolean;
9
- ci: boolean;
10
- prNumber?: number;
11
- base?: string;
12
- head?: string;
13
- references?: string[];
14
- verbose?: VerboseLevel;
15
- includes?: string[];
16
- llmMode?: LLMMode;
17
- files?: string[];
18
- commits?: string[];
19
- verifyFixes?: boolean;
20
- verifyConcurrency?: number;
21
- analyzeDeletions?: AnalyzeDeletionsMode;
22
- /** 仅执行删除代码分析,跳过常规代码审查 */
23
- deletionOnly?: boolean;
24
- /** 删除代码分析模式:openai 使用标准模式,claude-agent 使用 Agent 模式 */
25
- deletionAnalysisMode?: LLMMode;
26
- /** 输出格式:markdown, terminal, json。不指定则智能选择 */
27
- outputFormat?: ReportFormat;
28
- /** 是否使用 AI 生成 PR 功能描述 */
29
- generateDescription?: boolean;
30
- /** 显示所有问题,不过滤非变更行的问题 */
31
- showAll?: boolean;
32
- /** PR 事件类型(opened, synchronize, closed 等) */
33
- eventAction?: string;
34
- concurrency?: number;
35
- timeout?: number;
36
- retries?: number;
37
- retryDelay?: number;
38
- }
39
-
40
- /**
41
- * Review 命令
42
- *
43
- * 在 GitHub Actions 中执行,用于自动代码审查
44
- *
45
- * 环境变量:
46
- * - GITHUB_TOKEN: GitHub API Token
47
- * - GITHUB_REPOSITORY: 仓库名称 (owner/repo 格式)
48
- * - GITHUB_REF_NAME: 当前分支名称
49
- * - GITHUB_EVENT_PATH: 事件文件路径(包含 PR 信息)
50
- */
51
- @Command({
52
- name: "review",
53
- description: t("review:description"),
54
- })
55
- export class ReviewCommand extends CommandRunner {
56
- constructor(protected readonly reviewService: ReviewService) {
57
- super();
58
- }
59
-
60
- async run(_passedParams: string[], options: ReviewOptions): Promise<void> {
61
- try {
62
- const context = await this.reviewService.getContextFromEnv(options);
63
- await this.reviewService.execute(context);
64
- } catch (error) {
65
- if (error instanceof Error) {
66
- console.error(t("common.executionFailed", { error: error.message }));
67
- if (error.stack) {
68
- console.error(t("common.stackTrace", { stack: error.stack }));
69
- }
70
- } else {
71
- console.error(t("common.executionFailed", { error }));
72
- }
73
- process.exit(1);
74
- }
75
- }
76
-
77
- @Option({
78
- flags: "-d, --dry-run",
79
- description: t("review:options.dryRun"),
80
- })
81
- parseDryRun(val: boolean): boolean {
82
- return val;
83
- }
84
-
85
- @Option({
86
- flags: "-c, --ci",
87
- description: t("common.options.ci"),
88
- })
89
- parseCi(val: boolean): boolean {
90
- return val;
91
- }
92
-
93
- @Option({
94
- flags: "-p, --pr-number <number>",
95
- description: t("review:options.prNumber"),
96
- })
97
- parsePrNumber(val: string): number {
98
- return parseInt(val, 10);
99
- }
100
-
101
- @Option({
102
- flags: "-b, --base <ref>",
103
- description: t("review:options.base"),
104
- })
105
- parseBase(val: string): string {
106
- return val;
107
- }
108
-
109
- @Option({
110
- flags: "--head <ref>",
111
- description: t("review:options.head"),
112
- })
113
- parseHead(val: string): string {
114
- return val;
115
- }
116
-
117
- @Option({
118
- flags: "-v, --verbose",
119
- description: t("common.options.verboseDebug"),
120
- })
121
- parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
122
- const current = typeof previous === "number" ? previous : previous ? 1 : 0;
123
- return Math.min(current + 1, 3) as VerboseLevel;
124
- }
125
-
126
- @Option({
127
- flags: "-i, --includes <patterns...>",
128
- description: t("review:options.includes"),
129
- })
130
- parseIncludes(val: string, previous: string[] = []): string[] {
131
- return [...previous, val];
132
- }
133
-
134
- @Option({
135
- flags: "-l, --llm-mode <mode>",
136
- description: t("review:options.llmMode"),
137
- choices: ["claude-code", "openai", "gemini"],
138
- })
139
- parseLlmMode(val: string): LLMMode {
140
- return val as LLMMode;
141
- }
142
-
143
- @Option({
144
- flags: "-f, --files <files...>",
145
- description: t("review:options.files"),
146
- })
147
- parseFiles(val: string, previous: string[] = []): string[] {
148
- return [...previous, val];
149
- }
150
-
151
- @Option({
152
- flags: "--commits <commits...>",
153
- description: t("review:options.commits"),
154
- })
155
- parseCommits(val: string, previous: string[] = []): string[] {
156
- return [...previous, val];
157
- }
158
-
159
- @Option({
160
- flags: "--verify-fixes",
161
- description: t("review:options.verifyFixes"),
162
- })
163
- parseVerifyFixes(val: boolean): boolean {
164
- return val;
165
- }
166
-
167
- @Option({
168
- flags: "--no-verify-fixes",
169
- description: t("review:options.noVerifyFixes"),
170
- })
171
- parseNoVerifyFixes(val: boolean): boolean {
172
- return !val;
173
- }
174
-
175
- @Option({
176
- flags: "--verify-concurrency <number>",
177
- description: t("review:options.verifyConcurrency"),
178
- })
179
- parseVerifyConcurrency(val: string): number {
180
- return parseInt(val, 10);
181
- }
182
-
183
- @Option({
184
- flags: "--analyze-deletions [mode]",
185
- description: t("review:options.analyzeDeletions"),
186
- })
187
- parseAnalyzeDeletions(val: string | boolean): AnalyzeDeletionsMode {
188
- if (val === true || val === "true") return true;
189
- if (val === false || val === "false") return false;
190
- if (val === "ci" || val === "pr" || val === "terminal") return val;
191
- // 默认为 true(当只传 --analyze-deletions 不带值时)
192
- return true;
193
- }
194
-
195
- @Option({
196
- flags: "--deletion-analysis-mode <mode>",
197
- description: t("review:options.deletionAnalysisMode"),
198
- choices: ["openai", "claude-code"],
199
- })
200
- parseDeletionAnalysisMode(val: string): LLMMode {
201
- return val as LLMMode;
202
- }
203
-
204
- @Option({
205
- flags: "--deletion-only",
206
- description: t("review:options.deletionOnly"),
207
- })
208
- parseDeletionOnly(val: boolean): boolean {
209
- return val;
210
- }
211
-
212
- @Option({
213
- flags: "-o, --output-format <format>",
214
- description: t("review:options.outputFormat"),
215
- choices: ["markdown", "terminal", "json"],
216
- })
217
- parseOutputFormat(val: string): ReportFormat {
218
- return val as ReportFormat;
219
- }
220
-
221
- @Option({
222
- flags: "--generate-description",
223
- description: t("review:options.generateDescription"),
224
- })
225
- parseGenerateDescription(val: boolean): boolean {
226
- return val;
227
- }
228
-
229
- @Option({
230
- flags: "--show-all",
231
- description: t("review:options.showAll"),
232
- })
233
- parseShowAll(val: boolean): boolean {
234
- return val;
235
- }
236
-
237
- @Option({
238
- flags: "--event-action <action>",
239
- description: t("review:options.eventAction"),
240
- })
241
- parseEventAction(val: string): string {
242
- return val;
243
- }
244
- }
package/src/review.mcp.ts DELETED
@@ -1,184 +0,0 @@
1
- /**
2
- * Review MCP 服务
3
- * 提供代码审查规则查询的 MCP 工具
4
- */
5
-
6
- import { McpServer, McpTool, ConfigReaderService, t } from "@spaceflow/core";
7
- import { ReviewSpecService } from "./review-spec/review-spec.service";
8
- import { join } from "path";
9
- import { existsSync } from "fs";
10
- import { ListRulesInput, GetRulesForFileInput, GetRuleDetailInput } from "./dto/mcp.dto";
11
- import type { ReviewConfig } from "./review.config";
12
-
13
- @McpServer({ name: "review-mcp", version: "1.0.0", description: t("review:mcp.serverDescription") })
14
- export class ReviewMcp {
15
- constructor(
16
- private readonly specService: ReviewSpecService,
17
- private readonly configReader: ConfigReaderService,
18
- ) {}
19
-
20
- /**
21
- * 获取项目的规则目录
22
- */
23
- private async getSpecDirs(cwd: string): Promise<string[]> {
24
- const dirs: string[] = [];
25
-
26
- // 1. 通过 ConfigReaderService 读取 review 配置
27
- try {
28
- const reviewConfig = this.configReader.getPluginConfig<ReviewConfig>("review");
29
- if (reviewConfig?.references?.length) {
30
- const resolved = await this.specService.resolveSpecSources(reviewConfig.references);
31
- dirs.push(...resolved);
32
- }
33
- } catch {
34
- // 忽略配置读取错误
35
- }
36
-
37
- // 2. 检查默认目录
38
- const defaultDirs = [
39
- join(cwd, ".claude", "skills"),
40
- join(cwd, ".cursor", "skills"),
41
- join(cwd, "review-specs"),
42
- ];
43
-
44
- for (const dir of defaultDirs) {
45
- if (existsSync(dir)) {
46
- dirs.push(dir);
47
- }
48
- }
49
-
50
- return [...new Set(dirs)]; // 去重
51
- }
52
-
53
- /**
54
- * 加载所有规则
55
- */
56
- private async loadAllSpecs(cwd: string) {
57
- const specDirs = await this.getSpecDirs(cwd);
58
- const allSpecs = [];
59
-
60
- for (const dir of specDirs) {
61
- const specs = await this.specService.loadReviewSpecs(dir);
62
- allSpecs.push(...specs);
63
- }
64
-
65
- // 只去重,不应用 override(MCP 工具应返回所有规则,override 在实际审查时应用)
66
- return this.specService.deduplicateSpecs(allSpecs);
67
- }
68
-
69
- @McpTool({
70
- name: "list_rules",
71
- description: t("review:mcp.listRules"),
72
- dto: ListRulesInput,
73
- })
74
- async listRules(input: ListRulesInput) {
75
- const workDir = input.cwd || process.cwd();
76
- const specs = await this.loadAllSpecs(workDir);
77
-
78
- const rules = specs.flatMap((spec) =>
79
- spec.rules.map((rule) => ({
80
- id: rule.id,
81
- title: rule.title,
82
- description: rule.description.slice(0, 200) + (rule.description.length > 200 ? "..." : ""),
83
- severity: rule.severity || spec.severity,
84
- extensions: spec.extensions,
85
- specFile: spec.filename,
86
- includes: spec.includes,
87
- hasExamples: rule.examples.length > 0,
88
- })),
89
- );
90
-
91
- return {
92
- total: rules.length,
93
- rules,
94
- };
95
- }
96
-
97
- @McpTool({
98
- name: "get_rules_for_file",
99
- description: t("review:mcp.getRulesForFile"),
100
- dto: GetRulesForFileInput,
101
- })
102
- async getRulesForFile(input: GetRulesForFileInput) {
103
- const workDir = input.cwd || process.cwd();
104
- const allSpecs = await this.loadAllSpecs(workDir);
105
-
106
- // 根据文件过滤适用的规则
107
- const applicableSpecs = this.specService.filterApplicableSpecs(allSpecs, [
108
- { filename: input.filePath },
109
- ]);
110
-
111
- // 进一步根据 includes 过滤(支持规则级 includes 覆盖文件级)
112
- const micromatchModule = await import("micromatch");
113
- const micromatch = micromatchModule.default || micromatchModule;
114
-
115
- const rules = applicableSpecs.flatMap((spec) =>
116
- spec.rules
117
- .filter((rule) => {
118
- // 规则级 includes 优先于文件级
119
- const includes = rule.includes || spec.includes;
120
- if (includes.length === 0) return true;
121
- return micromatch.isMatch(input.filePath, includes, { matchBase: true });
122
- })
123
- .map((rule) => ({
124
- id: rule.id,
125
- title: rule.title,
126
- description: rule.description,
127
- severity: rule.severity || spec.severity,
128
- specFile: spec.filename,
129
- ...(input.includeExamples && rule.examples.length > 0
130
- ? {
131
- examples: rule.examples.map((ex) => ({
132
- type: ex.type,
133
- lang: ex.lang,
134
- code: ex.code,
135
- })),
136
- }
137
- : {}),
138
- })),
139
- );
140
-
141
- return {
142
- file: input.filePath,
143
- total: rules.length,
144
- rules,
145
- };
146
- }
147
-
148
- @McpTool({
149
- name: "get_rule_detail",
150
- description: t("review:mcp.getRuleDetail"),
151
- dto: GetRuleDetailInput,
152
- })
153
- async getRuleDetail(input: GetRuleDetailInput) {
154
- const workDir = input.cwd || process.cwd();
155
- const specs = await this.loadAllSpecs(workDir);
156
-
157
- const result = this.specService.findRuleById(input.ruleId, specs);
158
-
159
- if (!result) {
160
- return { error: t("review:mcp.ruleNotFound", { ruleId: input.ruleId }) };
161
- }
162
-
163
- const { rule, spec } = result;
164
-
165
- return {
166
- id: rule.id,
167
- title: rule.title,
168
- description: rule.description,
169
- severity: rule.severity || spec.severity,
170
- specFile: spec.filename,
171
- extensions: spec.extensions,
172
- includes: spec.includes,
173
- overrides: rule.overrides,
174
- examples: rule.examples.map((ex) => ({
175
- type: ex.type,
176
- lang: ex.lang,
177
- code: ex.code,
178
- })),
179
- };
180
- }
181
- }
182
-
183
- // ReviewMcpService 类已通过 @McpServer 装饰器标记
184
- // CLI 的 `spaceflow mcp` 命令会自动扫描并发现该类