@spaceflow/review 0.29.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +533 -0
  2. package/README.md +124 -0
  3. package/dist/551.js +9 -0
  4. package/dist/index.js +5704 -0
  5. package/package.json +50 -0
  6. package/src/README.md +364 -0
  7. package/src/__mocks__/@anthropic-ai/claude-agent-sdk.js +3 -0
  8. package/src/__mocks__/json-stringify-pretty-compact.ts +4 -0
  9. package/src/deletion-impact.service.spec.ts +974 -0
  10. package/src/deletion-impact.service.ts +879 -0
  11. package/src/dto/mcp.dto.ts +42 -0
  12. package/src/index.ts +32 -0
  13. package/src/issue-verify.service.spec.ts +460 -0
  14. package/src/issue-verify.service.ts +309 -0
  15. package/src/locales/en/review.json +31 -0
  16. package/src/locales/index.ts +11 -0
  17. package/src/locales/zh-cn/review.json +31 -0
  18. package/src/parse-title-options.spec.ts +251 -0
  19. package/src/parse-title-options.ts +185 -0
  20. package/src/review-report/formatters/deletion-impact.formatter.ts +144 -0
  21. package/src/review-report/formatters/index.ts +4 -0
  22. package/src/review-report/formatters/json.formatter.ts +8 -0
  23. package/src/review-report/formatters/markdown.formatter.ts +291 -0
  24. package/src/review-report/formatters/terminal.formatter.ts +130 -0
  25. package/src/review-report/index.ts +4 -0
  26. package/src/review-report/review-report.module.ts +8 -0
  27. package/src/review-report/review-report.service.ts +58 -0
  28. package/src/review-report/types.ts +26 -0
  29. package/src/review-spec/index.ts +3 -0
  30. package/src/review-spec/review-spec.module.ts +10 -0
  31. package/src/review-spec/review-spec.service.spec.ts +1543 -0
  32. package/src/review-spec/review-spec.service.ts +902 -0
  33. package/src/review-spec/types.ts +143 -0
  34. package/src/review.command.ts +244 -0
  35. package/src/review.config.ts +58 -0
  36. package/src/review.mcp.ts +184 -0
  37. package/src/review.module.ts +52 -0
  38. package/src/review.service.spec.ts +3007 -0
  39. package/src/review.service.ts +2603 -0
  40. package/tsconfig.json +8 -0
  41. package/vitest.config.ts +34 -0
@@ -0,0 +1,143 @@
1
+ export type Severity = "off" | "warn" | "error";
2
+
3
+ /** 文件内容行:[commitHash, lineCode],commitHash 为 7 位短 hash 或 "-------" 表示非变更行 */
4
+ export type FileContentLine = [string, string];
5
+
6
+ /** 文件内容映射:filename -> 每行的 [commitHash, lineCode] 数组 */
7
+ export type FileContentsMap = Map<string, FileContentLine[]>;
8
+
9
+ export const SEVERITY_EMOJI: Record<Severity, string> = {
10
+ off: "⚪",
11
+ warn: "🟡",
12
+ error: "🔴",
13
+ };
14
+
15
+ export interface ReviewSpec {
16
+ filename: string;
17
+ extensions: string[];
18
+ type: string;
19
+ content: string;
20
+ rules: ReviewRule[];
21
+ overrides: string[]; // 文件级别的 override
22
+ severity: Severity; // 文件级别的默认 severity
23
+ includes: string[]; // 文件级别的 includes,只有匹配的文件才应用此规范
24
+ }
25
+
26
+ export interface RuleExample {
27
+ lang: string;
28
+ code: string;
29
+ type: "good" | "bad";
30
+ }
31
+
32
+ export interface ReviewRule {
33
+ id: string;
34
+ title: string;
35
+ description: string;
36
+ examples: RuleExample[];
37
+ overrides: string[]; // 规则级别的 override
38
+ severity?: Severity; // 规则级别的 severity,可覆盖文件级别
39
+ includes?: string[]; // 规则级别的 includes,可覆盖文件级别
40
+ }
41
+
42
+ /** 问题的 Reaction 记录 */
43
+ export interface IssueReaction {
44
+ /** reaction 内容,如 +1, -1, laugh, hooray, confused, heart, rocket, eyes */
45
+ content: string;
46
+ /** 点击该 reaction 的用户列表 */
47
+ users: string[];
48
+ }
49
+
50
+ /** 用户信息 */
51
+ export interface UserInfo {
52
+ /** 用户 ID */
53
+ id?: string;
54
+ /** 用户登录名 */
55
+ login: string;
56
+ }
57
+
58
+ /** 问题评论的回复记录 */
59
+ export interface IssueReply {
60
+ /** 回复用户 */
61
+ user: UserInfo;
62
+ /** 回复内容 */
63
+ body: string;
64
+ /** 回复时间 */
65
+ createdAt: string;
66
+ }
67
+
68
+ export interface ReviewIssue {
69
+ file: string;
70
+ line: string; // 格式 12 或者 12-14
71
+ code: string; // 当前行代码, 去除首尾空白
72
+ ruleId: string;
73
+ specFile: string;
74
+ reason: string;
75
+ date?: string; // 发现问题的时间
76
+ fixed?: string; // 修复时间
77
+ valid?: string; // 问题是否有效
78
+ suggestion?: string;
79
+ commit?: string;
80
+ severity: Severity;
81
+ round: number; // 发现问题的轮次
82
+ /** 问题的作者 */
83
+ author?: UserInfo;
84
+ /** 问题评论的 reactions 记录 */
85
+ reactions?: IssueReaction[];
86
+ /** 问题评论的回复/聊天记录 */
87
+ replies?: IssueReply[];
88
+ /** 原始行号(行号更新前的值) */
89
+ originalLine?: string;
90
+ }
91
+
92
+ export interface FileSummary {
93
+ file: string;
94
+ resolved: number;
95
+ unresolved: number;
96
+ summary: string;
97
+ }
98
+
99
+ export interface DeletionImpact {
100
+ file: string;
101
+ deletedCode: string;
102
+ riskLevel: "high" | "medium" | "low" | "none";
103
+ affectedFiles: string[];
104
+ reason: string;
105
+ suggestion?: string;
106
+ }
107
+
108
+ export interface DeletionImpactResult {
109
+ impacts: DeletionImpact[];
110
+ summary: string;
111
+ }
112
+
113
+ /** Review 统计信息 */
114
+ export interface ReviewStats {
115
+ /** 总问题数 */
116
+ total: number;
117
+ /** 已修复数 */
118
+ fixed: number;
119
+ /** 无效问题数 */
120
+ invalid: number;
121
+ /** 待处理数 */
122
+ pending: number;
123
+ /** 修复率 (0-100) */
124
+ fixRate: number;
125
+ }
126
+
127
+ export interface ReviewResult {
128
+ success: boolean;
129
+ /**
130
+ * AI 生成的 PR 标题
131
+ */
132
+ title?: string;
133
+ /**
134
+ * 通过 commit 和 文件总结一下这个 PR 开发的什么功能
135
+ */
136
+ description: string;
137
+ issues: ReviewIssue[];
138
+ summary: FileSummary[];
139
+ deletionImpact?: DeletionImpactResult;
140
+ round: number; // 当前 review 的轮次
141
+ /** 问题统计信息 */
142
+ stats?: ReviewStats;
143
+ }
@@ -0,0 +1,244 @@
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
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from "@spaceflow/core";
2
+
3
+ /** LLM 模式 schema(与 core 中的 LLMMode 保持一致) */
4
+ const llmModeSchema = z.enum(["claude-code", "openai", "gemini", "open-code"]);
5
+
6
+ /** 删除代码分析模式 schema */
7
+ const analyzeDeletionsModeSchema = z.union([z.boolean(), z.enum(["ci", "pr", "terminal"])]);
8
+
9
+ /** 审查规则严重级别 schema */
10
+ const severitySchema = z.enum(["off", "warn", "error"]);
11
+
12
+ /** 变更文件处理策略 schema */
13
+ const invalidateChangedFilesSchema = z.enum(["invalidate", "keep", "off"]);
14
+
15
+ /** review 命令配置 schema(LLM 敏感配置由系统 llm.config.ts 管理) */
16
+ export const reviewSchema = () =>
17
+ z.object({
18
+ references: z.array(z.string()).optional(),
19
+ llmMode: llmModeSchema.default("openai").optional(),
20
+ includes: z.array(z.string()).optional(),
21
+ rules: z.record(z.string(), severitySchema).optional(),
22
+ verifyFixes: z.boolean().default(false),
23
+ verifyFixesConcurrency: z.number().default(10).optional(),
24
+ analyzeDeletions: analyzeDeletionsModeSchema.default(false),
25
+ deletionAnalysisMode: llmModeSchema.default("openai").optional(),
26
+ lineComments: z.boolean().default(false),
27
+ generateDescription: z.boolean().default(false).optional(),
28
+ autoUpdatePrTitle: z.boolean().default(false).optional(),
29
+ concurrency: z.number().default(5).optional(),
30
+ timeout: z.number().optional(),
31
+ retries: z.number().default(0).optional(),
32
+ retryDelay: z.number().default(1000).optional(),
33
+ invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
34
+ });
35
+
36
+ /**
37
+ * 变更文件处理策略
38
+ * - 'invalidate': 将变更文件的历史问题标记为无效(默认)
39
+ * - 'keep': 保留历史问题,不做处理
40
+ * - 'off': 关闭此功能
41
+ */
42
+ export type InvalidateChangedFilesMode = z.infer<typeof invalidateChangedFilesSchema>;
43
+
44
+ /** review 配置类型(从 schema 推导) */
45
+ 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>;
@@ -0,0 +1,184 @@
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` 命令会自动扫描并发现该类
@@ -0,0 +1,52 @@
1
+ import {
2
+ Module,
3
+ ConfigModule,
4
+ ConfigService,
5
+ ConfigReaderModule,
6
+ ConfigReaderService,
7
+ GitProviderModule,
8
+ ciConfig,
9
+ llmConfig,
10
+ ClaudeSetupModule,
11
+ LlmProxyModule,
12
+ GitSdkModule,
13
+ type LlmConfig,
14
+ } from "@spaceflow/core";
15
+ import { ReviewSpecModule } from "./review-spec";
16
+ import { ReviewReportModule } from "./review-report";
17
+ import { ReviewCommand } from "./review.command";
18
+ import { ReviewService } from "./review.service";
19
+ import { IssueVerifyService } from "./issue-verify.service";
20
+ import { DeletionImpactService } from "./deletion-impact.service";
21
+ import { ReviewMcp } from "./review.mcp";
22
+ import type { ReviewConfig } from "./review.config";
23
+
24
+ @Module({
25
+ imports: [
26
+ ConfigModule.forFeature(ciConfig),
27
+ ConfigModule.forFeature(llmConfig),
28
+ ConfigReaderModule,
29
+ GitProviderModule.forFeature(),
30
+ ClaudeSetupModule,
31
+ ReviewSpecModule,
32
+ ReviewReportModule,
33
+ GitSdkModule,
34
+ LlmProxyModule.forRootAsync({
35
+ imports: [ConfigReaderModule, ConfigModule],
36
+ useFactory: (configReader: ConfigReaderService, configService: ConfigService) => {
37
+ const reviewConf = configReader.getPluginConfig<ReviewConfig>("review");
38
+ const llm = configService.get<LlmConfig>("llm")!;
39
+ return {
40
+ defaultAdapter: reviewConf?.llmMode || "openai",
41
+ claudeCode: llm.claudeCode,
42
+ openai: llm.openai,
43
+ openCode: llm.openCode,
44
+ };
45
+ },
46
+ inject: [ConfigReaderService, ConfigService],
47
+ }),
48
+ ],
49
+ providers: [ReviewCommand, ReviewService, IssueVerifyService, DeletionImpactService, ReviewMcp],
50
+ exports: [ReviewMcp],
51
+ })
52
+ export class ReviewModule {}