@spaceflow/review-summary 0.21.0 → 0.23.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 CHANGED
@@ -1,5 +1,70 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.22.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review-summary@0.21.0...@spaceflow/review-summary@0.22.0) (2026-03-02)
4
+
5
+ ### 新特性
6
+
7
+ * **core:** Gitea适配器支持通过标签名称创建Issue ([cf10eda](https://github.com/Lydanne/spaceflow/commit/cf10eda5d025c560cc0d8e3826dad40716020d5c))
8
+ * **review-summary:** commit-based 评分新增修复问题加分机制,支持从 review 模块精确提取问题统计 ([304bf81](https://github.com/Lydanne/spaceflow/commit/304bf81ab475b280ab5f4011159bc697861bddf6))
9
+ * **review-summary:** 支持为周期统计报告 Issue 配置自定义标签 ([df1cc61](https://github.com/Lydanne/spaceflow/commit/df1cc61167851ff7106104914319f43f73ba8902))
10
+ * **review-summary:** 支持通过配置文件自定义评分权重 ([53e1a37](https://github.com/Lydanne/spaceflow/commit/53e1a371288aea6ceac63b03fda99eea1739be4b))
11
+ * **review-summary:** 新增 commit-based 评分策略,支持按有效 commit 累计计分 ([111c0d6](https://github.com/Lydanne/spaceflow/commit/111c0d6b9d87d12096e0edb69a11eceff55b79c1))
12
+ * **review:** 为行级评论 Review 添加统计信息摘要 ([58d5b37](https://github.com/Lydanne/spaceflow/commit/58d5b37ba54daa24bd2f8396318fedc87f388c74))
13
+ * **review:** 优化问题统计展示,按 severity 分级显示 error/warn 数量 ([bcb2608](https://github.com/Lydanne/spaceflow/commit/bcb26086589a67e815db075f3001209904572926))
14
+ * **review:** 保留历史行级评论,为每轮 Review 生成独立评论并添加上轮回顾 ([de431a0](https://github.com/Lydanne/spaceflow/commit/de431a09b4e3b5e1ada9ee5f1ee65786d22b6ff9))
15
+ * **review:** 支持用户手动 resolve 评论并在报告中区分 AI 修复与手动解决 ([c968b65](https://github.com/Lydanne/spaceflow/commit/c968b65c850bc68de3f4409aa3b5294e5a0311ff))
16
+ * **review:** 新增 MCP 工具支持从目录批量加载代码审查规则 ([289a836](https://github.com/Lydanne/spaceflow/commit/289a83650f1e222482fcbaaa69fb5ea562c5a4c2))
17
+ * **review:** 新增解决率统计指标,区分修复率和解决率的计算维度 ([436541f](https://github.com/Lydanne/spaceflow/commit/436541fce605319da562445a81242a8feb257df9))
18
+
19
+ ### 修复BUG
20
+
21
+ * **review:** 修复率计算仅统计 AI 修复的问题,排除手动解决的问题 ([12b3415](https://github.com/Lydanne/spaceflow/commit/12b3415749c9d8523e8b23365fbb39fc7657ff1d))
22
+ * **review:** 修正 PR 评论标题中的 emoji 显示问题 ([bcdc946](https://github.com/Lydanne/spaceflow/commit/bcdc9467bf7970c9acd3ea00303bcae5eaff131f))
23
+
24
+ ### 代码重构
25
+
26
+ * **review:** 抽取规则加载和问题验证逻辑为独立方法,优化代码复用性 ([7ea02ba](https://github.com/Lydanne/spaceflow/commit/7ea02ba86e369bc130c69c561195634072cc060a))
27
+
28
+ ### 文档更新
29
+
30
+ * **docs:** 为 review-summary 命令文档补充 Issue 输出配置说明 ([196fa94](https://github.com/Lydanne/spaceflow/commit/196fa94ad1ed2dbadbdcb332ef26cf1fe7fcd8d7))
31
+ * **review-summary:** 完善文档,新增时间预设、评分算法及输出示例说明 ([fb04685](https://github.com/Lydanne/spaceflow/commit/fb04685dde4157f0a1a2f8edaf1fb3c125280e27))
32
+ * **review:** 完善 review 命令文档,新增审查流程、多轮审查、问题生命周期等核心机制说明 ([d6b2a20](https://github.com/Lydanne/spaceflow/commit/d6b2a20802ab98e5ddb01937c0fe8b268c403c6f))
33
+
34
+ ### 其他修改
35
+
36
+ * **core:** released version 0.20.0 [no ci] ([b7ed239](https://github.com/Lydanne/spaceflow/commit/b7ed239455244cd96f2b59ef67886dd0bfc057a8))
37
+ * **publish:** released version 0.44.0 [no ci] ([5b29159](https://github.com/Lydanne/spaceflow/commit/5b29159b2f0129d2ce81329cf48734d3d56b226e))
38
+ * **review:** released version 0.54.0 [no ci] ([252269a](https://github.com/Lydanne/spaceflow/commit/252269a299f9e580b858e04814e7d9a13fed7736))
39
+ * **review:** released version 0.55.0 [no ci] ([0245743](https://github.com/Lydanne/spaceflow/commit/02457439788dd70925b91118f7d5936a61d0e0de))
40
+ * **review:** released version 0.56.0 [no ci] ([2481dec](https://github.com/Lydanne/spaceflow/commit/2481dec141b0d5f444b5815ab9598378ac3e0b12))
41
+ * **review:** released version 0.57.0 [no ci] ([238a831](https://github.com/Lydanne/spaceflow/commit/238a83165fa1810a9429b8d6a66a1f75c477ce22))
42
+ * **scripts:** released version 0.21.0 [no ci] ([1f0a213](https://github.com/Lydanne/spaceflow/commit/1f0a2139d155807451dc968de8213bafe2e4edb8))
43
+ * **scripts:** released version 0.22.0 [no ci] ([f482504](https://github.com/Lydanne/spaceflow/commit/f48250486906016b414a7b00aabac342c1399045))
44
+ * **shell:** released version 0.21.0 [no ci] ([b619af7](https://github.com/Lydanne/spaceflow/commit/b619af741e16053868a2eedd41f56d50134954d8))
45
+ * **shell:** released version 0.22.0 [no ci] ([e716369](https://github.com/Lydanne/spaceflow/commit/e716369f57bfa20e710d354245c54d3a80e701f4))
46
+
47
+ ## [0.21.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review-summary@0.20.0...@spaceflow/review-summary@0.21.0) (2026-03-02)
48
+
49
+ ### 修复BUG
50
+
51
+ * **core:** 重构配置 Schema 生成逻辑,使用 SpaceflowConfigSchema 作为基础 ([c73eb1c](https://github.com/Lydanne/spaceflow/commit/c73eb1ce5b6f212b8a932a15224db7e63822f8d0))
52
+
53
+ ### 测试用例
54
+
55
+ * **review:** 增强 AI 评论识别和过滤功能的测试覆盖 ([bda706b](https://github.com/Lydanne/spaceflow/commit/bda706b99aab113521afe6bcd386a590811e20a6))
56
+
57
+ ### 其他修改
58
+
59
+ * **core:** released version 0.19.0 [no ci] ([c8bfe6b](https://github.com/Lydanne/spaceflow/commit/c8bfe6ba20893e2c3cd383ed7e7d3217b0492eb6))
60
+ * **publish:** released version 0.43.0 [no ci] ([1074b9c](https://github.com/Lydanne/spaceflow/commit/1074b9c5fb21a447093ef23300c451d790710b33))
61
+ * **review:** released version 0.51.0 [no ci] ([c93be78](https://github.com/Lydanne/spaceflow/commit/c93be78f6f1df9cb5e3515cee58cda65cad1b00f))
62
+ * **review:** released version 0.52.0 [no ci] ([c86406f](https://github.com/Lydanne/spaceflow/commit/c86406f6934d5de4f198eadff66ee6c3f7cfbe0d))
63
+ * **review:** released version 0.53.0 [no ci] ([5a6af03](https://github.com/Lydanne/spaceflow/commit/5a6af03c260060ac1b1901bb7273f501ca0037c7))
64
+ * **review:** 移除 .spaceflow 目录及其配置文件 ([64b310d](https://github.com/Lydanne/spaceflow/commit/64b310d8a77614a259a8d7588a09169626efb3ae))
65
+ * **scripts:** released version 0.20.0 [no ci] ([e1fac49](https://github.com/Lydanne/spaceflow/commit/e1fac49257bf4a5902c5884ec0e054384a7859d6))
66
+ * **shell:** released version 0.20.0 [no ci] ([8b69b53](https://github.com/Lydanne/spaceflow/commit/8b69b5340fe99973add2bea3e7d53f2082d0da54))
67
+
3
68
  ## [0.20.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review-summary@0.19.0...@spaceflow/review-summary@0.20.0) (2026-02-27)
4
69
 
5
70
  ### 新特性
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaceflow/review-summary",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Spaceflow 审查统计命令,根据时间范围统计 PR 贡献情况,按人员汇总并排序",
5
5
  "license": "MIT",
6
6
  "author": "Lydanne",
@@ -14,12 +14,15 @@
14
14
  },
15
15
  "type": "module",
16
16
  "main": "./dist/index.js",
17
+ "dependencies": {
18
+ "@spaceflow/review": "0.58.0"
19
+ },
17
20
  "devDependencies": {
18
21
  "@types/node": "^22.15.0",
19
22
  "@spaceflow/cli": "0.38.0"
20
23
  },
21
24
  "peerDependencies": {
22
- "@spaceflow/core": "0.19.0"
25
+ "@spaceflow/core": "0.21.0"
23
26
  },
24
27
  "spaceflow": {
25
28
  "type": "flow",
@@ -1,6 +1,7 @@
1
1
  import { GitProviderService, shouldLog, normalizeVerbose } from "@spaceflow/core";
2
2
  import type { IConfigReader } from "@spaceflow/core";
3
3
  import type { PullRequest, Issue, CiConfig } from "@spaceflow/core";
4
+ import { MarkdownFormatter, type ReviewIssue } from "@spaceflow/review";
4
5
  import { writeFileSync } from "fs";
5
6
  import { join } from "path";
6
7
  import type {
@@ -11,24 +12,52 @@ import type {
11
12
  UserStats,
12
13
  OutputTarget,
13
14
  TimePreset,
15
+ WeightedScoreWeights,
16
+ CommitBasedWeights,
17
+ IssueBasedWeights,
18
+ DefectRateWeights,
19
+ ReviewSummaryConfig,
20
+ ScoreStrategy,
14
21
  } from "./types";
15
22
 
16
- /** 分数权重配置 */
17
- const SCORE_WEIGHTS = {
18
- /** 每个 PR 的基础分 */
23
+ /** 加权模式默认权重 */
24
+ const DEFAULT_WEIGHTED_WEIGHTS: Required<WeightedScoreWeights> = {
19
25
  prBase: 10,
20
- /** 每 100 行新增代码的分数 */
21
26
  additionsPer100: 2,
22
- /** 每 100 行删除代码的分数 */
23
27
  deletionsPer100: 1,
24
- /** 每个变更文件的分数 */
25
28
  changedFile: 0.5,
26
- /** 每个未修复问题的扣分 */
27
29
  issueDeduction: 3,
28
- /** 每个已修复问题的加分 */
29
30
  fixedBonus: 1,
30
31
  };
31
32
 
33
+ /** 分数累计模式默认权重 */
34
+ const DEFAULT_COMMIT_BASED_WEIGHTS: Required<CommitBasedWeights> = {
35
+ validCommit: 5,
36
+ errorDeduction: 2,
37
+ warnDeduction: 1,
38
+ errorFixedBonus: 1,
39
+ warnFixedBonus: 0.5,
40
+ minCommitLines: 5,
41
+ };
42
+
43
+ /** issue-based 模式默认权重 */
44
+ const DEFAULT_ISSUE_BASED_WEIGHTS: Required<IssueBasedWeights> = {
45
+ minBase: 60,
46
+ maxBase: 100,
47
+ capLines: 1000,
48
+ errorDeduction: 8,
49
+ warnDeduction: 3,
50
+ errorFixedBonus: 5,
51
+ warnFixedBonus: 2,
52
+ };
53
+
54
+ /** defect-rate 模式默认权重 */
55
+ const DEFAULT_DEFECT_RATE_WEIGHTS: Required<DefectRateWeights> = {
56
+ errorPenalty: 0.3,
57
+ warnPenalty: 0.1,
58
+ fixedDiscount: 0.05,
59
+ };
60
+
32
61
  /**
33
62
  * 周期统计服务
34
63
  */
@@ -55,7 +84,7 @@ export class PeriodSummaryService {
55
84
  const ciConf = this.config.get<CiConfig>("ci");
56
85
  const repository = ciConf?.repository;
57
86
  if (!repository) {
58
- throw new Error("缺少仓库配置,请通过 --repository 参数或环境变量 GITHUB_REPOSITORY 指定");
87
+ throw new Error("缺少仓库配置,请通过 --repository 参数或环境变量 GITHUB_REPOSITORY / GITEA_REPOSITORY 指定");
59
88
  }
60
89
  const parts = repository.split("/");
61
90
  owner = parts[0];
@@ -140,7 +169,8 @@ export class PeriodSummaryService {
140
169
  } catch {
141
170
  // 如果获取文件失败,使用默认值
142
171
  }
143
- const { issueCount, fixedCount } = await this.extractIssueStats(owner, repo, pr.number!);
172
+ const issueStats = await this.extractIssueStats(owner, repo, pr.number!);
173
+ const validCommitCount = await this.countValidCommits(owner, repo, pr.number!);
144
174
  return {
145
175
  number: pr.number!,
146
176
  title: pr.title ?? "",
@@ -149,8 +179,8 @@ export class PeriodSummaryService {
149
179
  additions,
150
180
  deletions,
151
181
  changedFiles,
152
- issueCount,
153
- fixedCount,
182
+ ...issueStats,
183
+ validCommitCount,
154
184
  description: this.extractDescription(pr),
155
185
  };
156
186
  }
@@ -162,31 +192,125 @@ export class PeriodSummaryService {
162
192
  owner: string,
163
193
  repo: string,
164
194
  prNumber: number,
165
- ): Promise<{ issueCount: number; fixedCount: number }> {
195
+ ): Promise<{
196
+ issueCount: number;
197
+ fixedCount: number;
198
+ errorCount: number;
199
+ warnCount: number;
200
+ fixedErrors: number;
201
+ fixedWarns: number;
202
+ }> {
203
+ const empty = { issueCount: 0, fixedCount: 0, errorCount: 0, warnCount: 0, fixedErrors: 0, fixedWarns: 0 };
166
204
  try {
167
205
  const comments = await this.gitProvider.listIssueComments(owner, repo, prNumber);
168
- let issueCount = 0;
169
- let fixedCount = 0;
206
+ // 优先从 review 模块嵌入的结构化数据中精确提取
207
+ const formatter = new MarkdownFormatter();
170
208
  for (const comment of comments) {
171
209
  const body = comment.body ?? "";
172
- const issueMatch = body.match(/发现\s*(\d+)\s*个问题/);
173
- if (issueMatch) {
174
- issueCount = Math.max(issueCount, parseInt(issueMatch[1], 10));
175
- }
176
- const fixedMatch = body.match(/已修复[::]\s*(\d+)/);
177
- if (fixedMatch) {
178
- fixedCount = Math.max(fixedCount, parseInt(fixedMatch[1], 10));
210
+ const parsed = formatter.parse(body);
211
+ if (parsed?.result?.issues) {
212
+ return this.computeIssueStatsFromReviewIssues(parsed.result.issues);
179
213
  }
180
- const statsMatch = body.match(/🔴\s*(\d+).*🟡\s*(\d+)/);
181
- if (statsMatch) {
182
- const errorCount = parseInt(statsMatch[1], 10);
183
- const warnCount = parseInt(statsMatch[2], 10);
184
- issueCount = Math.max(issueCount, errorCount + warnCount);
214
+ }
215
+ // 回退:没有结构化数据时,从评论文本正则提取(兼容旧数据)
216
+ return this.extractIssueStatsFromText(comments);
217
+ } catch {
218
+ return empty;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * 从 ReviewIssue 列表中精确计算各类问题统计
224
+ */
225
+ protected computeIssueStatsFromReviewIssues(issues: ReviewIssue[]): {
226
+ issueCount: number;
227
+ fixedCount: number;
228
+ errorCount: number;
229
+ warnCount: number;
230
+ fixedErrors: number;
231
+ fixedWarns: number;
232
+ } {
233
+ const errorCount = issues.filter((i) => i.severity === "error").length;
234
+ const warnCount = issues.filter((i) => i.severity === "warn").length;
235
+ const fixedErrors = issues.filter((i) => i.severity === "error" && i.fixed).length;
236
+ const fixedWarns = issues.filter((i) => i.severity === "warn" && i.fixed).length;
237
+ return {
238
+ issueCount: issues.length,
239
+ fixedCount: fixedErrors + fixedWarns,
240
+ errorCount,
241
+ warnCount,
242
+ fixedErrors,
243
+ fixedWarns,
244
+ };
245
+ }
246
+
247
+ /**
248
+ * 回退:从评论文本正则提取问题统计(兼容无结构化数据的旧评论)
249
+ */
250
+ protected extractIssueStatsFromText(
251
+ comments: { body?: string }[],
252
+ ): {
253
+ issueCount: number;
254
+ fixedCount: number;
255
+ errorCount: number;
256
+ warnCount: number;
257
+ fixedErrors: number;
258
+ fixedWarns: number;
259
+ } {
260
+ let issueCount = 0;
261
+ let fixedCount = 0;
262
+ let errorCount = 0;
263
+ let warnCount = 0;
264
+ for (const comment of comments) {
265
+ const body = comment.body ?? "";
266
+ const issueMatch = body.match(/发现\s*(\d+)\s*个问题/);
267
+ if (issueMatch) {
268
+ issueCount = Math.max(issueCount, parseInt(issueMatch[1], 10));
269
+ }
270
+ const fixedMatch = body.match(/已修复[::]\s*(\d+)/);
271
+ if (fixedMatch) {
272
+ fixedCount = Math.max(fixedCount, parseInt(fixedMatch[1], 10));
273
+ }
274
+ const statsMatch = body.match(/🔴\s*(\d+).*🟡\s*(\d+)/);
275
+ if (statsMatch) {
276
+ errorCount = Math.max(errorCount, parseInt(statsMatch[1], 10));
277
+ warnCount = Math.max(warnCount, parseInt(statsMatch[2], 10));
278
+ issueCount = Math.max(issueCount, errorCount + warnCount);
279
+ }
280
+ }
281
+ // 文本模式无法区分修复类型,统一设为 0
282
+ return { issueCount, fixedCount, errorCount, warnCount, fixedErrors: 0, fixedWarns: 0 };
283
+ }
284
+
285
+ /**
286
+ * 统计 PR 中有效 commit 数量(逐 commit 获取行数判断)
287
+ */
288
+ protected async countValidCommits(owner: string, repo: string, prNumber: number): Promise<number> {
289
+ const config = this.getStrategyConfig();
290
+ const minLines = config.commitBasedWeights?.minCommitLines ?? DEFAULT_COMMIT_BASED_WEIGHTS.minCommitLines;
291
+ try {
292
+ const commits = await this.gitProvider.getPullRequestCommits(owner, repo, prNumber);
293
+ let validCount = 0;
294
+ for (const commit of commits) {
295
+ if (!commit.sha) continue;
296
+ // 跳过 merge commit(commit message 以 "Merge" 开头)
297
+ if (commit.commit?.message?.startsWith("Merge")) continue;
298
+ try {
299
+ const commitInfo = await this.gitProvider.getCommit(owner, repo, commit.sha);
300
+ const totalLines = (commitInfo.files ?? []).reduce(
301
+ (sum, file) => sum + (file.additions ?? 0) + (file.deletions ?? 0),
302
+ 0,
303
+ );
304
+ if (totalLines >= minLines) {
305
+ validCount++;
306
+ }
307
+ } catch {
308
+ // 获取单个 commit 失败,跳过
185
309
  }
186
310
  }
187
- return { issueCount, fixedCount };
311
+ return validCount;
188
312
  } catch {
189
- return { issueCount: 0, fixedCount: 0 };
313
+ return 0;
190
314
  }
191
315
  }
192
316
 
@@ -216,6 +340,11 @@ export class PeriodSummaryService {
216
340
  totalChangedFiles: 0,
217
341
  totalIssues: 0,
218
342
  totalFixed: 0,
343
+ totalErrors: 0,
344
+ totalWarns: 0,
345
+ totalFixedErrors: 0,
346
+ totalFixedWarns: 0,
347
+ totalValidCommits: 0,
219
348
  score: 0,
220
349
  features: [],
221
350
  prs: [],
@@ -228,37 +357,157 @@ export class PeriodSummaryService {
228
357
  userStats.totalChangedFiles += pr.changedFiles;
229
358
  userStats.totalIssues += pr.issueCount;
230
359
  userStats.totalFixed += pr.fixedCount;
360
+ userStats.totalErrors += pr.errorCount;
361
+ userStats.totalWarns += pr.warnCount;
362
+ userStats.totalFixedErrors += pr.fixedErrors;
363
+ userStats.totalFixedWarns += pr.fixedWarns;
364
+ userStats.totalValidCommits += pr.validCommitCount;
231
365
  if (pr.description) {
232
366
  userStats.features.push(pr.description);
233
367
  }
234
368
  userStats.prs.push(pr);
235
369
  }
370
+ this.applyScoreStrategy(userMap);
371
+ return userMap;
372
+ }
373
+
374
+ /**
375
+ * 获取当前评分策略配置
376
+ */
377
+ protected getStrategyConfig(): ReviewSummaryConfig {
378
+ try {
379
+ return this.config.get<ReviewSummaryConfig>("review-summary") ?? {};
380
+ } catch {
381
+ return {};
382
+ }
383
+ }
384
+
385
+ /**
386
+ * 根据配置的策略计算所有用户的分数
387
+ */
388
+ protected applyScoreStrategy(userMap: Map<string, UserStats>): void {
389
+ const config = this.getStrategyConfig();
390
+ const strategy: ScoreStrategy = config.strategy ?? "weighted";
236
391
  for (const userStats of userMap.values()) {
237
- userStats.score = this.calculateScore(userStats);
392
+ switch (strategy) {
393
+ case "defect-rate": {
394
+ const rate = this.calculateDefectRate(userStats, config);
395
+ userStats.defectRate = rate;
396
+ userStats.score = Math.round((100 - rate) * 10) / 10;
397
+ break;
398
+ }
399
+ case "issue-based":
400
+ userStats.score = this.calculateIssueBasedScore(userStats, config);
401
+ break;
402
+ case "commit-based":
403
+ userStats.score = this.calculateCommitBasedScore(userStats, config);
404
+ break;
405
+ case "weighted":
406
+ default:
407
+ userStats.score = this.calculateWeightedScore(userStats, config);
408
+ break;
409
+ }
238
410
  }
239
- return userMap;
240
411
  }
241
412
 
242
413
  /**
243
- * 计算用户综合分数
414
+ * 加权模式:计算用户综合分数
244
415
  */
245
- protected calculateScore(stats: UserStats): number {
246
- const prScore = stats.prCount * SCORE_WEIGHTS.prBase;
247
- const additionsScore = (stats.totalAdditions / 100) * SCORE_WEIGHTS.additionsPer100;
248
- const deletionsScore = (stats.totalDeletions / 100) * SCORE_WEIGHTS.deletionsPer100;
249
- const filesScore = stats.totalChangedFiles * SCORE_WEIGHTS.changedFile;
416
+ protected calculateWeightedScore(stats: UserStats, config: ReviewSummaryConfig): number {
417
+ const weights = { ...DEFAULT_WEIGHTED_WEIGHTS, ...config.scoreWeights };
418
+ const prScore = stats.prCount * weights.prBase;
419
+ const additionsScore = (stats.totalAdditions / 100) * weights.additionsPer100;
420
+ const deletionsScore = (stats.totalDeletions / 100) * weights.deletionsPer100;
421
+ const filesScore = stats.totalChangedFiles * weights.changedFile;
250
422
  const unfixedIssues = stats.totalIssues - stats.totalFixed;
251
- const issueDeduction = unfixedIssues * SCORE_WEIGHTS.issueDeduction;
252
- const fixedBonus = stats.totalFixed * SCORE_WEIGHTS.fixedBonus;
423
+ const issueDeduction = unfixedIssues * weights.issueDeduction;
424
+ const fixedBonus = stats.totalFixed * weights.fixedBonus;
253
425
  const totalScore =
254
426
  prScore + additionsScore + deletionsScore + filesScore - issueDeduction + fixedBonus;
255
427
  return Math.max(0, Math.round(totalScore * 10) / 10);
256
428
  }
257
429
 
430
+ /**
431
+ * issue-based 模式:逐 PR 计算基础分(对数缩放) + 问题扣分/修复加分
432
+ */
433
+ protected calculateIssueBasedScore(stats: UserStats, config: ReviewSummaryConfig): number {
434
+ const weights = { ...DEFAULT_ISSUE_BASED_WEIGHTS, ...config.issueBasedWeights };
435
+ let totalScore = 0;
436
+ for (const pr of stats.prs) {
437
+ const baseScore = this.calculatePrBaseScore(
438
+ pr.additions + pr.deletions,
439
+ weights.minBase,
440
+ weights.maxBase,
441
+ weights.capLines,
442
+ );
443
+ const deduction =
444
+ pr.errorCount * weights.errorDeduction + pr.warnCount * weights.warnDeduction;
445
+ const bonus =
446
+ pr.fixedErrors * weights.errorFixedBonus + pr.fixedWarns * weights.warnFixedBonus;
447
+ totalScore += Math.max(0, baseScore - deduction + bonus);
448
+ }
449
+ return Math.round(totalScore * 10) / 10;
450
+ }
451
+
452
+ /**
453
+ * 计算单个 PR 的基础分(对数缩放,映射到 [minBase, maxBase] 区间)
454
+ */
455
+ protected calculatePrBaseScore(
456
+ totalLines: number,
457
+ minBase: number,
458
+ maxBase: number,
459
+ capLines: number,
460
+ ): number {
461
+ if (totalLines <= 0) return minBase;
462
+ const ratio = Math.min(1, Math.log2(1 + totalLines) / Math.log2(1 + capLines));
463
+ return minBase + (maxBase - minBase) * ratio;
464
+ }
465
+
466
+ /**
467
+ * defect-rate 模式:基于问题密度(每百行代码)计算缺陷率
468
+ */
469
+ protected calculateDefectRate(stats: UserStats, config: ReviewSummaryConfig): number {
470
+ const weights = { ...DEFAULT_DEFECT_RATE_WEIGHTS, ...config.defectRateWeights };
471
+ if (stats.prs.length === 0) return 0;
472
+ let complianceSum = 0;
473
+ for (const pr of stats.prs) {
474
+ const totalLines = pr.additions + pr.deletions;
475
+ const per100 = Math.max(1, totalLines / 100);
476
+ const penalty =
477
+ (pr.errorCount * weights.errorPenalty + pr.warnCount * weights.warnPenalty) / per100;
478
+ const recovery = (pr.fixedErrors + pr.fixedWarns) * weights.fixedDiscount;
479
+ const compliance = Math.min(1, Math.max(0, 1 - penalty + recovery));
480
+ complianceSum += compliance;
481
+ }
482
+ const avgCompliance = complianceSum / stats.prs.length;
483
+ return Math.round((1 - avgCompliance) * 1000) / 10;
484
+ }
485
+
486
+ /**
487
+ * 分数累计模式:按有效 commit 加分,按 error/warn 扣分
488
+ */
489
+ protected calculateCommitBasedScore(stats: UserStats, config: ReviewSummaryConfig): number {
490
+ const weights = { ...DEFAULT_COMMIT_BASED_WEIGHTS, ...config.commitBasedWeights };
491
+ const commitScore = stats.totalValidCommits * weights.validCommit;
492
+ const errorDeduction = stats.totalErrors * weights.errorDeduction;
493
+ const warnDeduction = stats.totalWarns * weights.warnDeduction;
494
+ const fixedBonus =
495
+ stats.totalFixedErrors * weights.errorFixedBonus +
496
+ stats.totalFixedWarns * weights.warnFixedBonus;
497
+ const totalScore = commitScore - errorDeduction - warnDeduction + fixedBonus;
498
+ return Math.max(0, Math.round(totalScore * 10) / 10);
499
+ }
500
+
258
501
  /**
259
502
  * 按分数排序用户统计
260
503
  */
261
504
  protected sortUserStats(userMap: Map<string, UserStats>): UserStats[] {
505
+ const config = this.getStrategyConfig();
506
+ if (config.strategy === "defect-rate") {
507
+ return Array.from(userMap.values()).sort(
508
+ (a, b) => (a.defectRate ?? 0) - (b.defectRate ?? 0),
509
+ );
510
+ }
262
511
  return Array.from(userMap.values()).sort((a, b) => b.score - a.score);
263
512
  }
264
513
 
@@ -393,18 +642,32 @@ export class PeriodSummaryService {
393
642
  lines.push("");
394
643
  lines.push(`🏆 贡献者排名`);
395
644
  lines.push(`${"─".repeat(60)}`);
396
- const header = [
397
- "排名".padEnd(4),
398
- "用户".padEnd(15),
399
- "PR数".padStart(5),
400
- "新增".padStart(8),
401
- "删除".padStart(8),
402
- "问题".padStart(5),
403
- "分数".padStart(8),
404
- ].join(" │ ");
645
+ const isDefectRate = result.userStats.some((u) => u.defectRate !== undefined);
646
+ const header = isDefectRate
647
+ ? [
648
+ "排名".padEnd(4),
649
+ "用户".padEnd(15),
650
+ "PR数".padStart(5),
651
+ "新增".padStart(8),
652
+ "删除".padStart(8),
653
+ "问题".padStart(5),
654
+ "缺陷率".padStart(8),
655
+ ].join(" │ ")
656
+ : [
657
+ "排名".padEnd(4),
658
+ "用户".padEnd(15),
659
+ "PR数".padStart(5),
660
+ "新增".padStart(8),
661
+ "删除".padStart(8),
662
+ "问题".padStart(5),
663
+ "分数".padStart(8),
664
+ ].join(" │ ");
405
665
  lines.push(header);
406
666
  lines.push("─".repeat(60));
407
667
  result.userStats.forEach((user, index) => {
668
+ const lastCol = isDefectRate
669
+ ? `${(user.defectRate ?? 0).toFixed(1)}%`.padStart(8)
670
+ : user.score.toFixed(1).padStart(8);
408
671
  const row = [
409
672
  `#${index + 1}`.padEnd(4),
410
673
  user.username.slice(0, 15).padEnd(15),
@@ -412,7 +675,7 @@ export class PeriodSummaryService {
412
675
  `+${user.totalAdditions}`.padStart(8),
413
676
  `-${user.totalDeletions}`.padStart(8),
414
677
  String(user.totalIssues).padStart(5),
415
- user.score.toFixed(1).padStart(8),
678
+ lastCol,
416
679
  ].join(" │ ");
417
680
  lines.push(row);
418
681
  });
@@ -445,13 +708,24 @@ export class PeriodSummaryService {
445
708
  lines.push("");
446
709
  lines.push(`## 🏆 贡献者排名`);
447
710
  lines.push("");
448
- lines.push(`| 排名 | 用户 | PR数 | 新增 | 删除 | 问题 | 分数 |`);
449
- lines.push(`|------|------|------|------|------|------|------|`);
450
- result.userStats.forEach((user, index) => {
451
- lines.push(
452
- `| #${index + 1} | ${user.username} | ${user.prCount} | +${user.totalAdditions} | -${user.totalDeletions} | ${user.totalIssues} | ${user.score.toFixed(1)} |`,
453
- );
454
- });
711
+ const isDefectRate = result.userStats.some((u) => u.defectRate !== undefined);
712
+ if (isDefectRate) {
713
+ lines.push(`| 排名 | 用户 | PR数 | 新增 | 删除 | 问题 | 缺陷率 |`);
714
+ lines.push(`|------|------|------|------|------|------|--------|`);
715
+ result.userStats.forEach((user, index) => {
716
+ lines.push(
717
+ `| #${index + 1} | ${user.username} | ${user.prCount} | +${user.totalAdditions} | -${user.totalDeletions} | ${user.totalIssues} | ${(user.defectRate ?? 0).toFixed(1)}% |`,
718
+ );
719
+ });
720
+ } else {
721
+ lines.push(`| 排名 | 用户 | PR数 | 新增 | 删除 | 问题 | 分数 |`);
722
+ lines.push(`|------|------|------|------|------|------|------|`);
723
+ result.userStats.forEach((user, index) => {
724
+ lines.push(
725
+ `| #${index + 1} | ${user.username} | ${user.prCount} | +${user.totalAdditions} | -${user.totalDeletions} | ${user.totalIssues} | ${user.score.toFixed(1)} |`,
726
+ );
727
+ });
728
+ }
455
729
  lines.push("");
456
730
  lines.push(`## 📋 功能摘要`);
457
731
  lines.push("");
@@ -497,9 +771,12 @@ export class PeriodSummaryService {
497
771
  content: string,
498
772
  ): Promise<{ type: OutputTarget; location: string }> {
499
773
  const title = `📊 周期统计报告: ${result.period.since} ~ ${result.period.until}`;
774
+ const config = this.getStrategyConfig();
775
+ const labelName = config.issueLabel ?? "report";
500
776
  const issue: Issue = await this.gitProvider.createIssue(context.owner, context.repo, {
501
777
  title,
502
778
  body: content,
779
+ labels: [labelName],
503
780
  });
504
781
  const location = issue.html_url ?? `#${issue.number}`;
505
782
  if (shouldLog(context.verbose, 1)) {
package/src/types.ts CHANGED
@@ -71,6 +71,16 @@ export interface PrStats {
71
71
  issueCount: number;
72
72
  /** 已修复的问题数 */
73
73
  fixedCount: number;
74
+ /** error 级别问题数 */
75
+ errorCount: number;
76
+ /** warn 级别问题数 */
77
+ warnCount: number;
78
+ /** 已修复的 error 问题数 */
79
+ fixedErrors: number;
80
+ /** 已修复的 warn 问题数 */
81
+ fixedWarns: number;
82
+ /** 有效 commit 数(单个 commit 新增+删除 >= minCommitLines 行) */
83
+ validCommitCount: number;
74
84
  /** PR 描述/功能摘要 */
75
85
  description: string;
76
86
  }
@@ -91,14 +101,108 @@ export interface UserStats {
91
101
  totalIssues: number;
92
102
  /** 总已修复问题数 */
93
103
  totalFixed: number;
104
+ /** 总 error 级别问题数 */
105
+ totalErrors: number;
106
+ /** 总 warn 级别问题数 */
107
+ totalWarns: number;
108
+ /** 总已修复 error 数 */
109
+ totalFixedErrors: number;
110
+ /** 总已修复 warn 数 */
111
+ totalFixedWarns: number;
112
+ /** 总有效 commit 数 */
113
+ totalValidCommits: number;
94
114
  /** 综合分数 */
95
115
  score: number;
116
+ /** 缺陷率(0-100),0 表示零缺陷,100 表示全部违规,仅 defect-rate 策略时填充 */
117
+ defectRate?: number;
96
118
  /** 功能摘要列表 */
97
119
  features: string[];
98
120
  /** 该用户的 PR 列表 */
99
121
  prs: PrStats[];
100
122
  }
101
123
 
124
+ /** 评分策略类型 */
125
+ export type ScoreStrategy = "weighted" | "commit-based" | "issue-based" | "defect-rate";
126
+
127
+ /** 加权模式权重配置 */
128
+ export interface WeightedScoreWeights {
129
+ /** 每个 PR 的基础分,默认 10 */
130
+ prBase?: number;
131
+ /** 每 100 行新增代码的分数,默认 2 */
132
+ additionsPer100?: number;
133
+ /** 每 100 行删除代码的分数,默认 1 */
134
+ deletionsPer100?: number;
135
+ /** 每个变更文件的分数,默认 0.5 */
136
+ changedFile?: number;
137
+ /** 每个未修复问题的扣分,默认 3 */
138
+ issueDeduction?: number;
139
+ /** 每个已修复问题的加分,默认 1 */
140
+ fixedBonus?: number;
141
+ }
142
+
143
+ /** 分数累计模式权重配置 */
144
+ export interface CommitBasedWeights {
145
+ /** 每个有效 commit 的加分,默认 5 */
146
+ validCommit?: number;
147
+ /** 每个 error 问题的扣分,默认 2 */
148
+ errorDeduction?: number;
149
+ /** 每个 warn 问题的扣分,默认 1 */
150
+ warnDeduction?: number;
151
+ /** 修复一个 error 问题的加分,默认为 errorDeduction 的一半(1) */
152
+ errorFixedBonus?: number;
153
+ /** 修复一个 warn 问题的加分,默认为 warnDeduction 的一半(0.5) */
154
+ warnFixedBonus?: number;
155
+ /** 有效 commit 的最低代码行数(新增+删除),默认 5 */
156
+ minCommitLines?: number;
157
+ }
158
+
159
+ /** @deprecated 使用 WeightedScoreWeights 代替 */
160
+ export type ScoreWeights = WeightedScoreWeights;
161
+
162
+ /** issue-based 模式权重配置 */
163
+ export interface IssueBasedWeights {
164
+ /** PR 基础分下限,默认 60 */
165
+ minBase?: number;
166
+ /** PR 基础分上限,默认 100 */
167
+ maxBase?: number;
168
+ /** 代码量封顶行数(additions + deletions),超过此值基础分为 maxBase,默认 1000 */
169
+ capLines?: number;
170
+ /** 每个 error 问题的扣分,默认 8 */
171
+ errorDeduction?: number;
172
+ /** 每个 warn 问题的扣分,默认 3 */
173
+ warnDeduction?: number;
174
+ /** 修复一个 error 问题的加分,默认 5 */
175
+ errorFixedBonus?: number;
176
+ /** 修复一个 warn 问题的加分,默认 2 */
177
+ warnFixedBonus?: number;
178
+ }
179
+
180
+ /** defect-rate 模式权重配置 */
181
+ export interface DefectRateWeights {
182
+ /** 每百行代码 1 个 error 降低的合规度,默认 0.3 */
183
+ errorPenalty?: number;
184
+ /** 每百行代码 1 个 warn 降低的合规度,默认 0.1 */
185
+ warnPenalty?: number;
186
+ /** 修复 1 个问题恢复的合规度,默认 0.05 */
187
+ fixedDiscount?: number;
188
+ }
189
+
190
+ /** review-summary 扩展配置 */
191
+ export interface ReviewSummaryConfig {
192
+ /** 评分策略,默认 "weighted" */
193
+ strategy?: ScoreStrategy;
194
+ /** 加权模式权重配置(strategy 为 "weighted" 时生效) */
195
+ scoreWeights?: WeightedScoreWeights;
196
+ /** 分数累计模式权重配置(strategy 为 "commit-based" 时生效) */
197
+ commitBasedWeights?: CommitBasedWeights;
198
+ /** issue-based 模式权重配置(strategy 为 "issue-based" 时生效) */
199
+ issueBasedWeights?: IssueBasedWeights;
200
+ /** defect-rate 模式权重配置(strategy 为 "defect-rate" 时生效) */
201
+ defectRateWeights?: DefectRateWeights;
202
+ /** 创建 Issue 时添加的标签名称,默认 "report" */
203
+ issueLabel?: string;
204
+ }
205
+
102
206
  /** 周期统计结果 */
103
207
  export interface PeriodSummaryResult {
104
208
  /** 统计周期 */