@spaceflow/review 0.29.3 → 0.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review",
3
- "version": "0.29.3",
3
+ "version": "0.31.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.19.4"
28
+ "@spaceflow/cli": "0.21.0"
30
29
  },
31
30
  "peerDependencies": {
32
- "@nestjs/common": "^11.0.1",
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.3.0"
39
32
  },
40
33
  "spaceflow": {
41
34
  "type": "flow",
@@ -1,6 +1,4 @@
1
- import { vi, type Mocked, type Mock } from "vitest";
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: Mocked<LlmProxyService>;
17
- let gitProvider: Mocked<GitProviderService>;
14
+ let llmProxyService: any;
15
+ let gitProvider: any;
18
16
 
19
- beforeEach(async () => {
20
- const mockLlmProxyService = {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ llmProxyService = {
21
20
  chatStream: vi.fn(),
22
21
  };
23
22
 
24
- const mockGitProvider = {
23
+ gitProvider = {
25
24
  getPullRequestFiles: vi.fn(),
26
25
  getPullRequestDiff: vi.fn(),
27
26
  };
28
27
 
29
- const module: TestingModule = await Test.createTestingModule({
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,131 @@
1
1
  import "./locales";
2
- import { SpaceflowExtension, SpaceflowExtensionMetadata, t } from "@spaceflow/core";
3
- import { ReviewModule } from "./review.module";
4
- import { reviewSchema } from "./review.config";
5
- /** review Extension 元数据 */
6
- export const reviewMetadata: SpaceflowExtensionMetadata = {
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: "--flush", description: t("review:options.flush") },
52
+ { flags: "--event-action <action>", description: t("review:options.eventAction") },
53
+ ],
54
+ run: async (_args, options, ctx) => {
55
+ const isFlush = !!options?.flush;
56
+ if (!ctx.hasService("gitProvider")) {
57
+ ctx.output.error(
58
+ "review 命令需要配置 Git Provider,请在 spaceflow.json 中配置 gitProvider 字段",
59
+ );
60
+ process.exit(1);
61
+ }
62
+ if (!isFlush && !ctx.hasService("llmProxy")) {
63
+ ctx.output.error("review 命令需要配置 LLM 服务,请在 spaceflow.json 中配置 llm 字段");
64
+ process.exit(1);
65
+ }
66
+
67
+ const gitProvider = ctx.getService<GitProviderService>("gitProvider");
68
+ const configReader = ctx.getService<ConfigReaderService>("config");
69
+ const llmProxy = ctx.hasService("llmProxy")
70
+ ? ctx.getService<LlmProxyService>("llmProxy")
71
+ : (undefined as unknown as LlmProxyService);
72
+ const gitSdk = ctx.hasService("gitSdk")
73
+ ? ctx.getService<GitSdkService>("gitSdk")
74
+ : undefined;
75
+
76
+ const reviewSpecService = new ReviewSpecService(gitProvider);
77
+ const reviewReportService = new ReviewReportService();
78
+ const issueVerifyService = new IssueVerifyService(llmProxy, reviewSpecService);
79
+ const deletionImpactService = new DeletionImpactService(llmProxy, gitProvider);
14
80
 
15
- export class ReviewExtension implements SpaceflowExtension {
16
- getMetadata(): SpaceflowExtensionMetadata {
17
- return reviewMetadata;
18
- }
81
+ const reviewService = new ReviewService(
82
+ gitProvider,
83
+ ctx.config,
84
+ configReader,
85
+ reviewSpecService,
86
+ llmProxy,
87
+ reviewReportService,
88
+ issueVerifyService,
89
+ deletionImpactService,
90
+ gitSdk!,
91
+ );
19
92
 
20
- getModule() {
21
- return ReviewModule;
22
- }
23
- }
93
+ const reviewOptions = {
94
+ dryRun: !!options?.dryRun,
95
+ ci: !!options?.ci,
96
+ prNumber: options?.prNumber ? parseInt(options.prNumber as string, 10) : undefined,
97
+ base: options?.base as string,
98
+ head: options?.head as string,
99
+ verbose: parseVerbose(options?.verbose as string | boolean | undefined),
100
+ includes: options?.includes as string[],
101
+ llmMode: options?.llmMode as LLMMode,
102
+ files: options?.files as string[],
103
+ commits: options?.commits as string[],
104
+ verifyFixes: options?.verifyFixes as boolean,
105
+ analyzeDeletions: options?.analyzeDeletions as AnalyzeDeletionsMode,
106
+ deletionAnalysisMode: options?.deletionAnalysisMode as LLMMode,
107
+ deletionOnly: !!options?.deletionOnly,
108
+ outputFormat: options?.outputFormat as ReportFormat,
109
+ generateDescription: !!options?.generateDescription,
110
+ showAll: !!options?.showAll,
111
+ flush: isFlush,
112
+ eventAction: options?.eventAction as string,
113
+ };
24
114
 
25
- export default ReviewExtension;
115
+ try {
116
+ const context = await reviewService.getContextFromEnv(reviewOptions);
117
+ await reviewService.execute(context);
118
+ } catch (error) {
119
+ if (error instanceof Error) {
120
+ ctx.output.error(t("common.executionFailed", { error: error.message }));
121
+ } else {
122
+ ctx.output.error(t("common.executionFailed", { error: String(error) }));
123
+ }
124
+ process.exit(1);
125
+ }
126
+ },
127
+ },
128
+ ],
129
+ });
26
130
 
27
- export * from "./review.module";
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";
131
+ export default extension;
@@ -1,7 +1,5 @@
1
- import { vi, type Mocked } from "vitest";
2
- import { Test, TestingModule } from "@nestjs/testing";
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: Mocked<LlmProxyService>;
11
+ let llmProxyService: any;
14
12
 
15
- beforeEach(async () => {
16
- const mockLlmProxyService = {
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
- const module: TestingModule = await Test.createTestingModule({
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,
@@ -17,6 +17,7 @@
17
17
  "options.outputFormat": "Output format: markdown, terminal, json. Auto-selected if not specified (markdown for PR, terminal for CLI)",
18
18
  "options.generateDescription": "Generate PR description using AI",
19
19
  "options.showAll": "Show all issues found, including those on unchanged lines",
20
+ "options.flush": "Only sync status (reactions, resolved conversations, replies), skip LLM review",
20
21
  "options.eventAction": "PR event type (opened, synchronize, closed, etc.), closed only collects stats without AI review",
21
22
  "extensionDescription": "Code review command using LLM for automated PR review",
22
23
  "mcp.serverDescription": "Code review rules query service",
@@ -17,6 +17,7 @@
17
17
  "options.outputFormat": "输出格式: markdown, terminal, json。不指定则智能选择(PR 用 markdown,终端用 terminal)",
18
18
  "options.generateDescription": "使用 AI 生成 PR 功能描述",
19
19
  "options.showAll": "显示所有发现的问题,不过滤非变更行的问题",
20
+ "options.flush": "仅刷新状态(同步 reactions、resolved conversations、replies),不执行 LLM 审查",
20
21
  "options.eventAction": "PR 事件类型(opened, synchronize, closed 等),closed 时仅收集统计不进行 AI 审查",
21
22
  "extensionDescription": "代码审查命令,使用 LLM 对 PR 代码进行自动审查",
22
23
  "mcp.serverDescription": "代码审查规则查询服务",
@@ -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,4 +1,3 @@
1
1
  export * from "./types";
2
2
  export * from "./formatters";
3
3
  export * from "./review-report.service";
4
- export * from "./review-report.module";
@@ -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();
@@ -1,3 +1,2 @@
1
1
  export * from "./types";
2
2
  export * from "./review-spec.service";
3
- export * from "./review-spec.module";
@@ -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(async () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
17
16
  gitProvider = {
18
17
  listRepositoryContents: vi.fn(),
19
18
  getFileContent: vi.fn(),
20
19
  };
21
- const module: TestingModule = await Test.createTestingModule({
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(@Optional() protected readonly gitProvider?: GitProviderService) {}
21
+ constructor(protected readonly gitProvider?: GitProviderService) {}
25
22
  /**
26
23
  * 检查规则 ID 是否匹配(精确匹配或前缀匹配)
27
24
  * 例如: "JsTs.FileName" 匹配 "JsTs.FileName" 和 "JsTs.FileName.UpperCamel"