@spaceflow/review 0.77.0 → 0.79.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/src/review-llm.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  createStreamLoggerState,
8
8
  shouldLog,
9
9
  type VerboseLevel,
10
- type LlmJsonPutSchema,
11
10
  LlmJsonPut,
12
11
  parallel,
13
12
  } from "@spaceflow/core";
@@ -23,52 +22,19 @@ import { readdir } from "fs/promises";
23
22
  import { dirname, extname } from "path";
24
23
  import micromatch from "micromatch";
25
24
  import type { FileReviewPrompt, ReviewPrompt, LLMReviewOptions } from "./types/review-llm";
26
- import { buildLinesWithNumbers, buildCommitsSection } from "./utils/review-llm";
25
+ import { buildLinesWithNumbers, buildCommitsSection, extractCodeBlocks } from "./utils/review-llm";
26
+ import { extractCodeBlockTypes, extractGlobsFromIncludes } from "./review-includes-filter";
27
+ import {
28
+ REVIEW_SCHEMA,
29
+ buildFileReviewPrompt,
30
+ buildPrDescriptionPrompt,
31
+ buildPrTitlePrompt,
32
+ } from "./prompt";
33
+ import type { SystemRules } from "./review.config";
34
+ import { applyStaticRules } from "./system-rules";
27
35
 
28
36
  export type { FileReviewPrompt, ReviewPrompt, LLMReviewOptions } from "./types/review-llm";
29
37
 
30
- const REVIEW_SCHEMA: LlmJsonPutSchema = {
31
- type: "object",
32
- properties: {
33
- issues: {
34
- type: "array",
35
- items: {
36
- type: "object",
37
- properties: {
38
- file: { type: "string", description: "发生问题的文件路径" },
39
- line: {
40
- type: "string",
41
- description:
42
- "问题所在的行号,只支持单行或多行 (如 123 或 123-125),不允许使用 `,` 分隔多个行号",
43
- },
44
- ruleId: { type: "string", description: "违反的规则 ID(如 JsTs.FileName.UpperCamel)" },
45
- specFile: {
46
- type: "string",
47
- description: "规则来源的规范文件名(如 js&ts.file-name.md)",
48
- },
49
- reason: { type: "string", description: "问题的简要概括" },
50
- suggestion: {
51
- type: "string",
52
- description:
53
- "修改后的完整代码片段。要求以代码为主体,并在代码中使用详细的中文注释解释逻辑改进点。不要包含 Markdown 反引号。",
54
- },
55
- commit: { type: "string", description: "相关的 7 位 commit SHA" },
56
- severity: {
57
- type: "string",
58
- description: "问题严重程度,根据规则文档中的 severity 标记确定",
59
- enum: ["error", "warn"],
60
- },
61
- },
62
- required: ["file", "line", "ruleId", "specFile", "reason"],
63
- additionalProperties: false,
64
- },
65
- },
66
- summary: { type: "string", description: "本次代码审查的整体总结" },
67
- },
68
- required: ["issues", "summary"],
69
- additionalProperties: false,
70
- };
71
-
72
38
  export class ReviewLlmProcessor {
73
39
  readonly llmJsonPut: LlmJsonPut<ReviewResult>;
74
40
 
@@ -109,8 +75,11 @@ export class ReviewLlmProcessor {
109
75
  }
110
76
 
111
77
  // 如果有 includes 配置,检查文件名是否匹配 includes 模式
78
+ // 需先提取纯 glob(去掉 added|/modified| 前缀,过滤 code-* 空串),避免 micromatch 报错
112
79
  if (spec.includes.length > 0) {
113
- return micromatch.isMatch(filename, spec.includes, { matchBase: true });
80
+ const globs = extractGlobsFromIncludes(spec.includes);
81
+ if (globs.length === 0) return true;
82
+ return micromatch.isMatch(filename, globs, { matchBase: true });
114
83
  }
115
84
 
116
85
  // 没有 includes 配置,扩展名匹配即可
@@ -118,122 +87,121 @@ export class ReviewLlmProcessor {
118
87
  });
119
88
  }
120
89
 
121
- /**
122
- * 构建 systemPrompt
123
- */
124
- buildSystemPrompt(specsSection: string): string {
125
- return `你是一个专业的代码审查专家,负责根据团队的编码规范对代码进行严格审查。
126
-
127
- ## 审查规范
128
-
129
- ${specsSection}
130
-
131
- ## 审查要求
132
-
133
- 1. **严格遵循规范**:只按照上述审查规范进行审查,不要添加规范之外的要求
134
- 2. **精准定位问题**:每个问题必须指明具体的行号,行号从文件内容中的 "行号|" 格式获取
135
- 3. **避免重复报告**:如果提示词中包含"上一次审查结果",请不要重复报告已存在的问题
136
- 4. **提供可行建议**:对于每个问题,提供具体的修改建议代码
137
-
138
- ## 注意事项
139
-
140
- - 变更文件内容已在上下文中提供,无需调用读取工具
141
- - 你可以读取项目中的其他文件以了解上下文
142
- - 不要调用编辑工具修改文件,你的职责是审查而非修改
143
- - 文件内容格式为 "CommitHash 行号| 代码",输出的 line 字段应对应原始行号
144
-
145
- ## 输出要求
146
-
147
- - 发现问题时:在 issues 数组中列出所有问题,每个问题包含 file、line、ruleId、specFile、reason、suggestion、severity
148
- - 无论是否发现问题:都必须在 summary 中提供该文件的审查总结,简要说明审查结果`;
149
- }
150
-
151
90
  async buildReviewPrompt(
152
91
  specs: ReviewSpec[],
153
92
  changedFiles: ChangedFile[],
154
93
  fileContents: FileContentsMap,
155
94
  commits: PullRequestCommit[],
156
95
  existingResult?: ReviewResult | null,
96
+ whenModifiedCode?: string[],
97
+ verbose?: VerboseLevel,
98
+ systemRules?: SystemRules,
157
99
  ): Promise<ReviewPrompt> {
100
+ const round = (existingResult?.round ?? 0) + 1;
101
+ const { staticIssues, skippedFiles } = applyStaticRules(
102
+ changedFiles,
103
+ fileContents,
104
+ systemRules,
105
+ round,
106
+ verbose,
107
+ );
108
+
158
109
  const fileDataList = changedFiles
159
110
  .filter((f) => f.status !== "deleted" && f.filename)
160
111
  .map((file) => {
161
112
  const filename = file.filename!;
113
+ if (skippedFiles.has(filename)) return null;
162
114
  const contentLines = fileContents.get(filename);
163
115
  if (!contentLines) {
164
- return {
165
- filename,
166
- file,
167
- linesWithNumbers: "(无法获取内容)",
168
- commitsSection: "- 无相关 commits",
169
- };
116
+ return { filename, file, contentLines: null, commitsSection: "- 无相关 commits" };
170
117
  }
171
- const linesWithNumbers = buildLinesWithNumbers(contentLines);
172
118
  const commitsSection = buildCommitsSection(contentLines, commits);
173
- return { filename, file, linesWithNumbers, commitsSection };
119
+ return { filename, file, contentLines, commitsSection };
174
120
  });
175
121
 
176
- const filePrompts: FileReviewPrompt[] = await Promise.all(
177
- fileDataList.map(async ({ filename, file, linesWithNumbers, commitsSection }) => {
178
- const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
179
-
180
- // 获取该文件上一次的审查结果
181
- const existingFileSummary = existingResult?.summary?.find((s) => s.file === filename);
182
- const existingFileIssues = existingResult?.issues?.filter((i) => i.file === filename) ?? [];
183
-
184
- let previousReviewSection = "";
185
- if (existingFileSummary || existingFileIssues.length > 0) {
186
- const parts: string[] = [];
187
- if (existingFileSummary?.summary) {
188
- parts.push(`**总结**:\n`);
189
- parts.push(`${existingFileSummary.summary}\n`);
190
- }
191
- if (existingFileIssues.length > 0) {
192
- parts.push(`**已发现的问题** (${existingFileIssues.length} ):\n`);
193
- for (const issue of existingFileIssues) {
194
- const status = issue.fixed
195
- ? "✅ 已修复"
196
- : issue.valid === "false"
197
- ? "❌ 无效"
198
- : "⚠️ 待处理";
199
- parts.push(`- [${status}] 行 ${issue.line}: ${issue.reason} (规则: ${issue.ruleId})`);
122
+ const filePrompts: (FileReviewPrompt | null)[] = await Promise.all(
123
+ fileDataList
124
+ .filter((item): item is NonNullable<typeof item> => item !== null)
125
+ .map(async ({ filename, file, contentLines, commitsSection }) => {
126
+ const fileDirectoryInfo = await this.getFileDirectoryInfo(filename);
127
+
128
+ // 根据文件过滤 specs,只注入与当前文件匹配的规则
129
+ const fileSpecs = this.filterSpecsForFile(specs, filename);
130
+
131
+ // 从全局 whenModifiedCode 配置中解析代码结构过滤类型
132
+ const codeBlockTypes = whenModifiedCode ? extractCodeBlockTypes(whenModifiedCode) : [];
133
+
134
+ // 构建带行号的内容:有 code-* 过滤时只输出匹配的代码块范围
135
+ let linesWithNumbers: string;
136
+ if (!contentLines) {
137
+ linesWithNumbers = "(无法获取内容)";
138
+ } else if (codeBlockTypes.length > 0) {
139
+ const visibleRanges = extractCodeBlocks(contentLines, codeBlockTypes);
140
+ // 如果配置了 whenModifiedCode 但没有匹配的代码块,跳过这个文件
141
+ if (visibleRanges.length === 0) {
142
+ if (shouldLog(verbose, 2)) {
143
+ console.log(
144
+ `[buildReviewPrompt] ${filename}: 没有匹配的 ${codeBlockTypes.join(", ")} 代码块,跳过审查`,
145
+ );
146
+ }
147
+ return null;
200
148
  }
201
- parts.push("");
202
- // parts.push("请注意:不要重复报告上述已发现的问题,除非代码有新的变更导致问题复现。\n");
149
+ linesWithNumbers = buildLinesWithNumbers(contentLines, visibleRanges);
150
+ } else {
151
+ linesWithNumbers = buildLinesWithNumbers(contentLines);
203
152
  }
204
- previousReviewSection = parts.join("\n");
205
- }
206
153
 
207
- const userPrompt = `## ${filename} (${file.status})
208
-
209
- ### 文件内容
210
-
211
- \`\`\`
212
- ${linesWithNumbers}
213
- \`\`\`
214
-
215
- ### 该文件的相关 Commits
216
-
217
- ${commitsSection}
218
-
219
- ### 该文件所在的目录树
220
-
221
- ${fileDirectoryInfo}
222
-
223
- ### 上一次审查结果
224
-
225
- ${previousReviewSection}`;
226
-
227
- // 根据文件过滤 specs,只注入与当前文件匹配的规则
228
- const fileSpecs = this.filterSpecsForFile(specs, filename);
229
- const specsSection = this.reviewSpecService.buildSpecsSection(fileSpecs);
230
- const systemPrompt = this.buildSystemPrompt(specsSection);
154
+ // 获取该文件上一次的审查结果
155
+ const existingFileSummary = existingResult?.summary?.find((s) => s.file === filename);
156
+ const existingFileIssues =
157
+ existingResult?.issues?.filter((i) => i.file === filename) ?? [];
158
+
159
+ let previousReviewSection = "";
160
+ if (existingFileSummary || existingFileIssues.length > 0) {
161
+ const parts: string[] = [];
162
+ if (existingFileSummary?.summary) {
163
+ parts.push(`**总结**:\n`);
164
+ parts.push(`${existingFileSummary.summary}\n`);
165
+ }
166
+ if (existingFileIssues.length > 0) {
167
+ parts.push(`**已发现的问题** (${existingFileIssues.length} 个):\n`);
168
+ for (const issue of existingFileIssues) {
169
+ const status = issue.fixed
170
+ ? "✅ 已修复"
171
+ : issue.valid === "false"
172
+ ? "❌ 无效"
173
+ : "⚠️ 待处理";
174
+ parts.push(
175
+ `- [${status}] ${issue.line}: ${issue.reason} (规则: ${issue.ruleId})`,
176
+ );
177
+ }
178
+ parts.push("");
179
+ // parts.push("请注意:不要重复报告上述已发现的问题,除非代码有新的变更导致问题复现。\n");
180
+ }
181
+ previousReviewSection = parts.join("\n");
182
+ }
231
183
 
232
- return { filename, systemPrompt, userPrompt };
233
- }),
184
+ const specsSection = this.reviewSpecService.buildSpecsSection(fileSpecs);
185
+ const { systemPrompt, userPrompt } = buildFileReviewPrompt({
186
+ filename,
187
+ status: file.status,
188
+ linesWithNumbers,
189
+ commitsSection,
190
+ fileDirectoryInfo,
191
+ previousReviewSection,
192
+ specsSection,
193
+ });
194
+
195
+ return { filename, systemPrompt, userPrompt };
196
+ }),
234
197
  );
235
198
 
236
- return { filePrompts };
199
+ // 过滤掉 null 值(跳过的文件)
200
+ const validFilePrompts = filePrompts.filter((fp): fp is FileReviewPrompt => fp !== null);
201
+ return {
202
+ filePrompts: validFilePrompts,
203
+ staticIssues: staticIssues.length > 0 ? staticIssues : undefined,
204
+ };
237
205
  }
238
206
 
239
207
  async runLLMReview(
@@ -475,54 +443,14 @@ ${previousReviewSection}`;
475
443
  fileContents?: FileContentsMap,
476
444
  verbose?: VerboseLevel,
477
445
  ): Promise<{ title: string; description: string }> {
478
- const commitMessages = commits
479
- .map((c) => `- ${c.sha?.slice(0, 7)}: ${c.commit?.message?.split("\n")[0]}`)
480
- .join("\n");
481
- const fileChanges = changedFiles
482
- .slice(0, 30)
483
- .map((f) => `- ${f.filename} (${f.status})`)
484
- .join("\n");
485
- // 构建代码变更内容(只包含变更行,限制总长度)
486
- let codeChangesSection = "";
487
- if (fileContents && fileContents.size > 0) {
488
- const codeSnippets: string[] = [];
489
- let totalLength = 0;
490
- const maxTotalLength = 8000; // 限制代码总长度
491
- for (const [filename, lines] of fileContents) {
492
- if (totalLength >= maxTotalLength) break;
493
- // 只提取有变更的行(commitHash 不是 "-------")
494
- const changedLines = lines
495
- .map(([hash, code], idx) => (hash !== "-------" ? `${idx + 1}: ${code}` : null))
496
- .filter(Boolean);
497
- if (changedLines.length > 0) {
498
- const snippet = `### ${filename}\n\`\`\`\n${changedLines.slice(0, 50).join("\n")}\n\`\`\``;
499
- if (totalLength + snippet.length <= maxTotalLength) {
500
- codeSnippets.push(snippet);
501
- totalLength += snippet.length;
502
- }
503
- }
504
- }
505
- if (codeSnippets.length > 0) {
506
- codeChangesSection = `\n\n## 代码变更内容\n${codeSnippets.join("\n\n")}`;
507
- }
508
- }
509
- const prompt = `请根据以下 PR 的 commit 记录、文件变更和代码内容,用简洁的中文总结这个 PR 实现了什么功能。
510
- 要求:
511
- 1. 第一行输出 PR 标题,格式必须是: Feat xxx 或 Fix xxx 或 Refactor xxx(根据变更类型选择,整体不超过 50 个字符)
512
- 2. 空一行后输出详细描述
513
- 3. 描述应该简明扼要,突出核心功能点
514
- 4. 使用 Markdown 格式
515
- 5. 不要逐条列出 commit,而是归纳总结
516
- 6. 重点分析代码变更的实际功能
517
-
518
- ## Commit 记录 (${commits.length} 个)
519
- ${commitMessages || "无"}
520
-
521
- ## 文件变更 (${changedFiles.length} 个文件)
522
- ${fileChanges || "无"}
523
- ${changedFiles.length > 30 ? `\n... 等 ${changedFiles.length - 30} 个文件` : ""}${codeChangesSection}`;
446
+ const { userPrompt } = buildPrDescriptionPrompt({
447
+ commits,
448
+ changedFiles,
449
+ fileContents,
450
+ });
451
+
524
452
  try {
525
- const stream = this.llmProxyService.chatStream([{ role: "user", content: prompt }], {
453
+ const stream = this.llmProxyService.chatStream([{ role: "user", content: userPrompt }], {
526
454
  adapter: llmMode,
527
455
  });
528
456
  let content = "";
@@ -553,29 +481,10 @@ ${changedFiles.length > 30 ? `\n... 等 ${changedFiles.length - 30} 个文件` :
553
481
  commits: PullRequestCommit[],
554
482
  changedFiles: ChangedFile[],
555
483
  ): Promise<string> {
556
- const commitMessages = commits
557
- .slice(0, 10)
558
- .map((c) => c.commit?.message?.split("\n")[0])
559
- .filter(Boolean)
560
- .join("\n");
561
- const fileChanges = changedFiles
562
- .slice(0, 20)
563
- .map((f) => `${f.filename} (${f.status})`)
564
- .join("\n");
565
- const prompt = `请根据以下 commit 记录和文件变更,生成一个简短的 PR 标题。
566
- 要求:
567
- 1. 格式必须是: Feat: xxx 或 Fix: xxx 或 Refactor: xxx
568
- 2. 根据变更内容选择合适的前缀(新功能用 Feat,修复用 Fix,重构用 Refactor)
569
- 3. xxx 部分用简短的中文描述(整体不超过 50 个字符)
570
- 4. 只输出标题,不要加任何解释
571
-
572
- Commit 记录:
573
- ${commitMessages || "无"}
574
-
575
- 文件变更:
576
- ${fileChanges || "无"}`;
484
+ const { userPrompt } = buildPrTitlePrompt({ commits, changedFiles });
485
+
577
486
  try {
578
- const stream = this.llmProxyService.chatStream([{ role: "user", content: prompt }], {
487
+ const stream = this.llmProxyService.chatStream([{ role: "user", content: userPrompt }], {
579
488
  adapter: "openai",
580
489
  });
581
490
  let title = "";
@@ -19,7 +19,7 @@ import {
19
19
  calculateIssueStats,
20
20
  REVIEW_COMMENT_MARKER,
21
21
  REVIEW_LINE_COMMENTS_MARKER,
22
- } from "./review-pr-comment-utils";
22
+ } from "./utils/review-pr-comment";
23
23
 
24
24
  export interface ReviewResultSaveOptions {
25
25
  verbose?: VerboseLevel;
@@ -325,11 +325,33 @@ export class ReviewResultModel {
325
325
  // 遍历每个评论,获取其 reactions
326
326
  for (const comment of reviewComments) {
327
327
  if (!comment.id) continue;
328
- // 找到对应的 issue
329
- const matchedIssue = this._result.issues.find(
330
- (issue) =>
331
- issue.file === comment.path && this.lineMatchesPosition(issue.line, comment.position),
332
- );
328
+ // 找到对应的 issue:优先通过 issue-key 精确匹配,回退到 path+line 匹配
329
+ let matchedIssue: ReviewIssue | undefined;
330
+ if (comment.body) {
331
+ const issueKey = extractIssueKeyFromBody(comment.body);
332
+ if (issueKey) {
333
+ matchedIssue = this._result.issues.find(
334
+ (issue) => generateIssueKey(issue) === issueKey,
335
+ );
336
+ if (shouldLog(verbose, 3)) {
337
+ console.log(
338
+ `[syncReactionsToIssues] comment ${comment.id}: issue-key=${issueKey}, matched=${matchedIssue ? "yes" : "no"}`,
339
+ );
340
+ }
341
+ }
342
+ }
343
+ // 如果 issue-key 匹配失败,使用 path+position 回退匹配
344
+ if (!matchedIssue) {
345
+ matchedIssue = this._result.issues.find(
346
+ (issue) =>
347
+ issue.file === comment.path && this.lineMatchesPosition(issue.line, comment.position),
348
+ );
349
+ if (shouldLog(verbose, 3)) {
350
+ console.log(
351
+ `[syncReactionsToIssues] comment ${comment.id}: fallback matching path=${comment.path}, position=${comment.position}, matched=${matchedIssue ? "yes" : "no"}`,
352
+ );
353
+ }
354
+ }
333
355
  if (matchedIssue) {
334
356
  commentIdToIssue.set(comment.id, matchedIssue);
335
357
  }
@@ -13,6 +13,7 @@ import { homedir } from "os";
13
13
  import { execSync } from "child_process";
14
14
  import micromatch from "micromatch";
15
15
  import { ReviewSpec, ReviewRule, RuleExample, Severity } from "./types";
16
+ import { extractGlobsFromIncludes } from "../review-includes-filter";
16
17
 
17
18
  /** 远程规则缓存 TTL(毫秒),默认 5 分钟 */
18
19
  const REMOTE_SPEC_CACHE_TTL = 5 * 60 * 1000;
@@ -69,7 +70,23 @@ export class ReviewSpecService {
69
70
  }
70
71
  }
71
72
 
72
- return specs.filter((spec) => spec.extensions.some((ext) => changedExtensions.has(ext)));
73
+ console.log(
74
+ `[filterApplicableSpecs] changedExtensions=${JSON.stringify([...changedExtensions])}, specs count=${specs.length}`,
75
+ );
76
+ const result = specs.filter((spec) => {
77
+ const matches = spec.extensions.some((ext) => changedExtensions.has(ext));
78
+ if (!matches) {
79
+ console.log(
80
+ `[filterApplicableSpecs] spec ${spec.filename} (ext: ${JSON.stringify(spec.extensions)}) NOT matched`,
81
+ );
82
+ } else {
83
+ console.log(
84
+ `[filterApplicableSpecs] spec ${spec.filename} (ext: ${JSON.stringify(spec.extensions)}) MATCHED`,
85
+ );
86
+ }
87
+ return matches;
88
+ });
89
+ return result;
73
90
  }
74
91
 
75
92
  async loadReviewSpecs(specDir: string): Promise<ReviewSpec[]> {
@@ -524,8 +541,10 @@ export class ReviewSpecService {
524
541
  return true;
525
542
  }
526
543
 
527
- // 检查文件是否匹配 includes 模式
528
- const matches = micromatch.isMatch(issue.file, includes, { matchBase: true });
544
+ // 检查文件是否匹配 includes 模式(转换为纯 glob,避免 status| 前缀和 code-* 空串传入 micromatch)
545
+ const globs = extractGlobsFromIncludes(includes);
546
+ if (globs.length === 0) return true;
547
+ const matches = micromatch.isMatch(issue.file, globs, { matchBase: true });
529
548
  if (!matches) {
530
549
  // console.log(` Issue [${issue.ruleId}] 在文件 ${issue.file} 不匹配 includes 模式,跳过`);
531
550
  }
@@ -638,8 +657,10 @@ export class ReviewSpecService {
638
657
  if (scoped.includes.length === 0) {
639
658
  return true;
640
659
  }
641
- // 使用 micromatch 检查文件是否匹配 includes 模式
642
- return issueFile && micromatch.isMatch(issueFile, scoped.includes, { matchBase: true });
660
+ // 使用 micromatch 检查文件是否匹配 includes 模式(转换为纯 glob)
661
+ const globs = extractGlobsFromIncludes(scoped.includes);
662
+ if (globs.length === 0) return true;
663
+ return issueFile && micromatch.isMatch(issueFile, globs, { matchBase: true });
643
664
  });
644
665
 
645
666
  if (matched) {
@@ -27,6 +27,15 @@ export type AnalyzeDeletionsMode = z.infer<typeof analyzeDeletionsModeSchema>;
27
27
  /** 审查规则严重级别 */
28
28
  export type Severity = z.infer<typeof severitySchema>;
29
29
 
30
+ /**
31
+ * 系统规则配置,不依赖 LLM,在构建 prompt 前直接检查并生成系统问题。
32
+ * 格式为 [阈值, severity]
33
+ */
34
+ export interface SystemRules {
35
+ /** 单文件最大审查行数,超过时跳过 LLM 并生成系统问题。格式: [maxLine, severity] */
36
+ maxLinesPerFile?: [number, Severity];
37
+ }
38
+
30
39
  /**
31
40
  * 变更文件处理策略
32
41
  * - 'invalidate': 将变更文件的历史问题标记为无效(默认)
@@ -47,6 +56,11 @@ export interface ReviewOptions {
47
56
  references?: string[];
48
57
  verbose?: VerboseLevel;
49
58
  includes?: string[];
59
+ /**
60
+ * 代码结构过滤配置,指定在代码审查时要关注的代码结构类型
61
+ * 支持格式:"function"、"class"、"interface"、"type"、"method"
62
+ */
63
+ whenModifiedCode?: string[];
50
64
  llmMode?: LLMMode;
51
65
  files?: string[];
52
66
  commits?: string[];
@@ -80,11 +94,12 @@ export interface ReviewOptions {
80
94
  */
81
95
  local?: LocalReviewMode;
82
96
  /**
83
- * 跳过重复的 review workflow 检查
84
- * - true: 启用检查,当检测到同名 workflow 正在运行时跳过审查
85
- * - false: 禁用检查(默认)
97
+ * 处理重复 workflow 的策略
98
+ * - 'off': 禁用检查
99
+ * - 'skip': 检测到同名 workflow 正在运行时跳过审查
100
+ * - 'delete': 检测到同名 workflow 时删除旧的 AI Review 评论和 PR Review(默认)
86
101
  */
87
- skipDuplicateWorkflow?: boolean;
102
+ duplicateWorkflowResolved?: "off" | "skip" | "delete";
88
103
  /**
89
104
  * 自动批准合并
90
105
  * - true: 当所有问题都已解决时,自动提交 APPROVE review
@@ -99,6 +114,8 @@ export interface ReviewOptions {
99
114
  * - 'warn+error': 有未解决的 warn 或 error 级别问题时抛出异常
100
115
  */
101
116
  failOnIssues?: "off" | "warn" | "error" | "warn+error";
117
+ /** 系统规则配置,不依赖 LLM,直接在检查阶段生成系统问题 */
118
+ systemRules?: SystemRules;
102
119
  }
103
120
 
104
121
  /** review 命令配置 schema(LLM 敏感配置由系统 llm.config.ts 管理) */
@@ -107,6 +124,7 @@ export const reviewSchema = () =>
107
124
  references: z.array(z.string()).optional(),
108
125
  llmMode: llmModeSchema.default("openai").optional(),
109
126
  includes: z.array(z.string()).optional(),
127
+ whenModifiedCode: z.array(z.string()).optional(),
110
128
  rules: z.record(z.string(), severitySchema).optional(),
111
129
  verifyFixes: z.boolean().default(false),
112
130
  verifyFixesConcurrency: z.number().default(10).optional(),
@@ -120,9 +138,17 @@ export const reviewSchema = () =>
120
138
  retries: z.number().default(0).optional(),
121
139
  retryDelay: z.number().default(1000).optional(),
122
140
  invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
123
- skipDuplicateWorkflow: z.boolean().default(false).optional(),
141
+ duplicateWorkflowResolved: z.enum(["off", "skip", "delete"]).default("delete").optional(),
124
142
  autoApprove: z.boolean().default(false).optional(),
125
143
  failOnIssues: z.enum(["off", "warn", "error", "warn+error"]).default("off").optional(),
144
+ systemRules: z
145
+ .object({
146
+ maxLinesPerFile: z
147
+ .tuple([z.number(), severitySchema])
148
+ .transform((v): [number, Severity] => [v[0], v[1]])
149
+ .optional(),
150
+ })
151
+ .optional(),
126
152
  });
127
153
 
128
154
  /** review 配置类型(从 schema 推导) */