@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 +65 -0
- package/package.json +5 -2
- package/src/review-summary.service.ts +333 -56
- package/src/types.ts +104 -0
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
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<{
|
|
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
|
-
|
|
169
|
-
|
|
206
|
+
// 优先从 review 模块嵌入的结构化数据中精确提取
|
|
207
|
+
const formatter = new MarkdownFormatter();
|
|
170
208
|
for (const comment of comments) {
|
|
171
209
|
const body = comment.body ?? "";
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
311
|
+
return validCount;
|
|
188
312
|
} catch {
|
|
189
|
-
return
|
|
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
|
-
|
|
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
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
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 *
|
|
252
|
-
const fixedBonus = stats.totalFixed *
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
lines.push(
|
|
452
|
-
|
|
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
|
/** 统计周期 */
|