@spaceflow/review 0.76.0 → 0.78.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/index.js +3830 -2469
  3. package/package.json +2 -2
  4. package/src/deletion-impact.service.ts +17 -130
  5. package/src/index.ts +34 -2
  6. package/src/issue-verify.service.ts +18 -82
  7. package/src/locales/en/review.json +2 -1
  8. package/src/locales/zh-cn/review.json +2 -1
  9. package/src/mcp/index.ts +4 -1
  10. package/src/prompt/code-review.ts +95 -0
  11. package/src/prompt/deletion-impact.ts +105 -0
  12. package/src/prompt/index.ts +37 -0
  13. package/src/prompt/issue-verify.ts +86 -0
  14. package/src/prompt/pr-description.ts +149 -0
  15. package/src/prompt/schemas.ts +106 -0
  16. package/src/prompt/types.ts +53 -0
  17. package/src/pull-request-model.ts +236 -0
  18. package/src/review-context.ts +433 -0
  19. package/src/review-includes-filter.spec.ts +284 -0
  20. package/src/review-includes-filter.ts +196 -0
  21. package/src/review-issue-filter.ts +523 -0
  22. package/src/review-llm.ts +543 -0
  23. package/src/review-result-model.spec.ts +657 -0
  24. package/src/review-result-model.ts +1046 -0
  25. package/src/review-spec/review-spec.service.ts +26 -5
  26. package/src/review-spec/types.ts +2 -0
  27. package/src/review.config.ts +40 -5
  28. package/src/review.service.spec.ts +102 -1625
  29. package/src/review.service.ts +608 -2742
  30. package/src/system-rules/index.ts +48 -0
  31. package/src/system-rules/max-lines-per-file.ts +57 -0
  32. package/src/types/review-llm.ts +21 -0
  33. package/src/utils/review-llm.spec.ts +277 -0
  34. package/src/utils/review-llm.ts +177 -0
  35. package/src/utils/review-pr-comment.spec.ts +340 -0
  36. package/src/utils/review-pr-comment.ts +186 -0
  37. package/tsconfig.json +1 -1
@@ -0,0 +1,340 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ REVIEW_COMMENT_MARKER,
4
+ REVIEW_LINE_COMMENTS_MARKER,
5
+ extractIssueKeyFromBody,
6
+ isAiGeneratedComment,
7
+ generateIssueKey,
8
+ syncRepliesToIssues,
9
+ deleteExistingAiReviews,
10
+ calculateIssueStats,
11
+ } from "./review-pr-comment";
12
+
13
+ describe("utils/review-pr-comment", () => {
14
+ // ─── 常量 ────────────────────────────────────────────────
15
+
16
+ describe("markers", () => {
17
+ it("REVIEW_COMMENT_MARKER 包含预期字符串", () => {
18
+ expect(REVIEW_COMMENT_MARKER).toBe("<!-- spaceflow-review -->");
19
+ });
20
+
21
+ it("REVIEW_LINE_COMMENTS_MARKER 包含预期字符串", () => {
22
+ expect(REVIEW_LINE_COMMENTS_MARKER).toBe("<!-- spaceflow-review-lines -->");
23
+ });
24
+ });
25
+
26
+ // ─── extractIssueKeyFromBody ─────────────────────────────
27
+
28
+ describe("extractIssueKeyFromBody", () => {
29
+ it("提取标准格式的 issue key", () => {
30
+ expect(extractIssueKeyFromBody("<!-- issue-key: src/a.ts:10:R1 -->")).toBe(
31
+ "src/a.ts:10:R1",
32
+ );
33
+ });
34
+
35
+ it("body 不含 issue-key 时返回 null", () => {
36
+ expect(extractIssueKeyFromBody("普通评论内容")).toBeNull();
37
+ });
38
+
39
+ it("issue key 含空格时正确提取(trim)", () => {
40
+ expect(extractIssueKeyFromBody("<!-- issue-key: a.ts:1:R1 -->")).toBe("a.ts:1:R1");
41
+ });
42
+ });
43
+
44
+ // ─── isAiGeneratedComment ────────────────────────────────
45
+
46
+ describe("isAiGeneratedComment", () => {
47
+ it("含 issue-key 标记时返回 true", () => {
48
+ expect(isAiGeneratedComment("some body <!-- issue-key: a.ts:1:R1 --> end")).toBe(true);
49
+ });
50
+
51
+ it("同时含规则和文件字段时返回 true", () => {
52
+ expect(isAiGeneratedComment("- **规则**: R1\n- **文件**: a.ts:1")).toBe(true);
53
+ });
54
+
55
+ it("普通用户评论返回 false", () => {
56
+ expect(isAiGeneratedComment("LGTM")).toBe(false);
57
+ });
58
+
59
+ it("空字符串返回 false", () => {
60
+ expect(isAiGeneratedComment("")).toBe(false);
61
+ });
62
+
63
+ it("只含规则字段(不含文件字段)返回 false", () => {
64
+ expect(isAiGeneratedComment("- **规则**: R1")).toBe(false);
65
+ });
66
+ });
67
+
68
+ // ─── generateIssueKey ───────────────────────────────────
69
+
70
+ describe("generateIssueKey", () => {
71
+ it("拼接 file:line:ruleId", () => {
72
+ expect(
73
+ generateIssueKey({ file: "src/a.ts", line: "10", ruleId: "R1" } as any),
74
+ ).toBe("src/a.ts:10:R1");
75
+ });
76
+ });
77
+
78
+ // ─── calculateIssueStats ────────────────────────────────
79
+
80
+ describe("calculateIssueStats", () => {
81
+ it("空数组返回全零统计", () => {
82
+ const stats = calculateIssueStats([]);
83
+ expect(stats).toEqual({
84
+ total: 0,
85
+ validTotal: 0,
86
+ fixed: 0,
87
+ resolved: 0,
88
+ invalid: 0,
89
+ pending: 0,
90
+ fixRate: 0,
91
+ resolveRate: 0,
92
+ });
93
+ });
94
+
95
+ it("全部待处理问题", () => {
96
+ const issues = [
97
+ { file: "a.ts", line: "1", ruleId: "R1" },
98
+ { file: "b.ts", line: "2", ruleId: "R2" },
99
+ ] as any[];
100
+ const stats = calculateIssueStats(issues);
101
+ expect(stats.total).toBe(2);
102
+ expect(stats.validTotal).toBe(2);
103
+ expect(stats.pending).toBe(2);
104
+ expect(stats.fixed).toBe(0);
105
+ expect(stats.resolved).toBe(0);
106
+ expect(stats.invalid).toBe(0);
107
+ });
108
+
109
+ it("valid=false 的问题计入 invalid,不计入 pending", () => {
110
+ const issues = [
111
+ { file: "a.ts", line: "1", ruleId: "R1", valid: "false" },
112
+ { file: "b.ts", line: "2", ruleId: "R2" },
113
+ ] as any[];
114
+ const stats = calculateIssueStats(issues);
115
+ expect(stats.total).toBe(2);
116
+ expect(stats.invalid).toBe(1);
117
+ expect(stats.validTotal).toBe(1);
118
+ expect(stats.pending).toBe(1);
119
+ });
120
+
121
+ it("fixed 问题计入 fixed,不计入 pending", () => {
122
+ const issues = [
123
+ { file: "a.ts", line: "1", ruleId: "R1", fixed: "2024-01-01" },
124
+ { file: "b.ts", line: "2", ruleId: "R2" },
125
+ ] as any[];
126
+ const stats = calculateIssueStats(issues);
127
+ expect(stats.fixed).toBe(1);
128
+ expect(stats.pending).toBe(1);
129
+ });
130
+
131
+ it("resolved(非 fixed)计入 resolved,不计入 pending", () => {
132
+ const issues = [
133
+ { file: "a.ts", line: "1", ruleId: "R1", resolved: "2024-01-01" },
134
+ ] as any[];
135
+ const stats = calculateIssueStats(issues);
136
+ expect(stats.resolved).toBe(1);
137
+ expect(stats.pending).toBe(0);
138
+ });
139
+
140
+ it("fixed 同时有 resolved 时只计入 fixed", () => {
141
+ const issues = [
142
+ { file: "a.ts", line: "1", ruleId: "R1", fixed: "2024-01-01", resolved: "2024-01-01" },
143
+ ] as any[];
144
+ const stats = calculateIssueStats(issues);
145
+ expect(stats.fixed).toBe(1);
146
+ expect(stats.resolved).toBe(0);
147
+ });
148
+
149
+ it("fixRate 计算正确(2/4 = 50%)", () => {
150
+ const issues = Array.from({ length: 4 }, (_, i) => ({
151
+ file: "a.ts",
152
+ line: String(i + 1),
153
+ ruleId: "R1",
154
+ ...(i < 2 ? { fixed: "2024-01-01" } : {}),
155
+ })) as any[];
156
+ const stats = calculateIssueStats(issues);
157
+ expect(stats.fixRate).toBe(50);
158
+ });
159
+ });
160
+
161
+ // ─── syncRepliesToIssues ─────────────────────────────────
162
+
163
+ describe("syncRepliesToIssues", () => {
164
+ const lineMatchesPosition = (line: string, pos?: number) =>
165
+ pos !== undefined && String(pos) === line;
166
+
167
+ it("单条评论(无回复)不产生 replies", async () => {
168
+ const issue = { file: "a.ts", line: "1", ruleId: "R1" } as any;
169
+ const result = { issues: [issue] } as any;
170
+ const comments = [
171
+ {
172
+ id: 1,
173
+ path: "a.ts",
174
+ position: 1,
175
+ body: `🔴 **问题**\n<!-- issue-key: a.ts:1:R1 -->`,
176
+ created_at: "2024-01-01T00:00:00Z",
177
+ },
178
+ ];
179
+ await syncRepliesToIssues(comments, result, lineMatchesPosition);
180
+ expect(issue.replies).toBeUndefined();
181
+ });
182
+
183
+ it("用户回复通过 issue-key 精确匹配后加入 replies", async () => {
184
+ const issue = { file: "a.ts", line: "1", ruleId: "R1" } as any;
185
+ const result = { issues: [issue] } as any;
186
+ const comments = [
187
+ {
188
+ id: 1,
189
+ path: "a.ts",
190
+ position: 1,
191
+ body: `🔴 问题\n<!-- issue-key: a.ts:1:R1 -->`,
192
+ created_at: "2024-01-01T00:00:00Z",
193
+ },
194
+ {
195
+ id: 2,
196
+ path: "a.ts",
197
+ position: 1,
198
+ body: "已修复",
199
+ user: { id: 42, login: "dev1" },
200
+ created_at: "2024-01-01T01:00:00Z",
201
+ },
202
+ ];
203
+ await syncRepliesToIssues(comments, result, lineMatchesPosition);
204
+ expect(issue.replies).toHaveLength(1);
205
+ expect(issue.replies[0].body).toBe("已修复");
206
+ expect(issue.replies[0].user.login).toBe("dev1");
207
+ });
208
+
209
+ it("AI 格式特征的评论不被计为用户回复", async () => {
210
+ const issue = { file: "a.ts", line: "1", ruleId: "R1" } as any;
211
+ const result = { issues: [issue] } as any;
212
+ const comments = [
213
+ {
214
+ id: 1,
215
+ path: "a.ts",
216
+ position: 1,
217
+ body: `🔴 问题\n<!-- issue-key: a.ts:1:R1 -->`,
218
+ created_at: "2024-01-01T00:00:00Z",
219
+ },
220
+ {
221
+ id: 2,
222
+ path: "a.ts",
223
+ position: 1,
224
+ body: "- **规则**: R2\n- **文件**: b.ts:2",
225
+ created_at: "2024-01-01T01:00:00Z",
226
+ },
227
+ ];
228
+ await syncRepliesToIssues(comments, result, lineMatchesPosition);
229
+ expect(issue.replies).toBeUndefined();
230
+ });
231
+
232
+ it("同一 issue 多条用户回复全部追加", async () => {
233
+ const issue = { file: "a.ts", line: "1", ruleId: "R1" } as any;
234
+ const result = { issues: [issue] } as any;
235
+ const comments = [
236
+ {
237
+ id: 1,
238
+ path: "a.ts",
239
+ position: 1,
240
+ body: `<!-- issue-key: a.ts:1:R1 -->`,
241
+ created_at: "2024-01-01T00:00:00Z",
242
+ },
243
+ {
244
+ id: 2,
245
+ path: "a.ts",
246
+ position: 1,
247
+ body: "回复1",
248
+ user: { login: "u1" },
249
+ created_at: "2024-01-01T01:00:00Z",
250
+ },
251
+ {
252
+ id: 3,
253
+ path: "a.ts",
254
+ position: 1,
255
+ body: "回复2",
256
+ user: { login: "u2" },
257
+ created_at: "2024-01-01T02:00:00Z",
258
+ },
259
+ ];
260
+ await syncRepliesToIssues(comments, result, lineMatchesPosition);
261
+ expect(issue.replies).toHaveLength(2);
262
+ });
263
+ });
264
+
265
+ // ─── deleteExistingAiReviews ─────────────────────────────
266
+
267
+ describe("deleteExistingAiReviews", () => {
268
+ let pr: any;
269
+
270
+ beforeEach(() => {
271
+ pr = {
272
+ getReviews: vi.fn(),
273
+ deleteReview: vi.fn(),
274
+ getComments: vi.fn(),
275
+ deleteComment: vi.fn(),
276
+ };
277
+ });
278
+
279
+ it("删除含 REVIEW_LINE_COMMENTS_MARKER 的 PR Review", async () => {
280
+ pr.getReviews.mockResolvedValue([
281
+ { id: 1, body: `${REVIEW_LINE_COMMENTS_MARKER} Round 1` },
282
+ { id: 2, body: "普通 review" },
283
+ ]);
284
+ pr.deleteReview.mockResolvedValue(undefined);
285
+ pr.getComments.mockResolvedValue([]);
286
+
287
+ await deleteExistingAiReviews(pr);
288
+
289
+ expect(pr.deleteReview).toHaveBeenCalledTimes(1);
290
+ expect(pr.deleteReview).toHaveBeenCalledWith(1);
291
+ });
292
+
293
+ it("删除含 REVIEW_COMMENT_MARKER 的 Issue Comment", async () => {
294
+ pr.getReviews.mockResolvedValue([]);
295
+ pr.getComments.mockResolvedValue([
296
+ { id: 10, body: `${REVIEW_COMMENT_MARKER} 报告内容` },
297
+ { id: 11, body: "普通评论" },
298
+ ]);
299
+ pr.deleteComment.mockResolvedValue(undefined);
300
+
301
+ await deleteExistingAiReviews(pr);
302
+
303
+ expect(pr.deleteComment).toHaveBeenCalledTimes(1);
304
+ expect(pr.deleteComment).toHaveBeenCalledWith(10);
305
+ });
306
+
307
+ it("deleteReview 失败时静默忽略,继续处理其他", async () => {
308
+ pr.getReviews.mockResolvedValue([
309
+ { id: 1, body: REVIEW_LINE_COMMENTS_MARKER },
310
+ { id: 2, body: REVIEW_LINE_COMMENTS_MARKER },
311
+ ]);
312
+ pr.deleteReview
313
+ .mockRejectedValueOnce(new Error("submitted review cannot be deleted"))
314
+ .mockResolvedValueOnce(undefined);
315
+ pr.getComments.mockResolvedValue([]);
316
+
317
+ await expect(deleteExistingAiReviews(pr)).resolves.not.toThrow();
318
+ expect(pr.deleteReview).toHaveBeenCalledTimes(2);
319
+ });
320
+
321
+ it("getReviews 失败时静默忽略,继续处理 comments", async () => {
322
+ pr.getReviews.mockRejectedValue(new Error("API error"));
323
+ pr.getComments.mockResolvedValue([{ id: 10, body: REVIEW_COMMENT_MARKER }]);
324
+ pr.deleteComment.mockResolvedValue(undefined);
325
+
326
+ await expect(deleteExistingAiReviews(pr)).resolves.not.toThrow();
327
+ expect(pr.deleteComment).toHaveBeenCalledWith(10);
328
+ });
329
+
330
+ it("无 AI 评论时不调用任何 delete", async () => {
331
+ pr.getReviews.mockResolvedValue([{ id: 1, body: "普通 review" }]);
332
+ pr.getComments.mockResolvedValue([{ id: 10, body: "普通评论" }]);
333
+
334
+ await deleteExistingAiReviews(pr);
335
+
336
+ expect(pr.deleteReview).not.toHaveBeenCalled();
337
+ expect(pr.deleteComment).not.toHaveBeenCalled();
338
+ });
339
+ });
340
+ });
@@ -0,0 +1,186 @@
1
+ import { ReviewIssue, ReviewResult, ReviewStats } from "../review-spec";
2
+ import { PullRequestModel } from "../pull-request-model";
3
+
4
+ export const REVIEW_COMMENT_MARKER = "<!-- spaceflow-review -->";
5
+ export const REVIEW_LINE_COMMENTS_MARKER = "<!-- spaceflow-review-lines -->";
6
+
7
+ /**
8
+ * 从评论 body 中提取 issue key(AI 行级评论末尾的 HTML 注释标记)
9
+ * 格式:`<!-- issue-key: file:line:ruleId -->`
10
+ * 返回 null 表示非 AI 评论(即用户真实回复)
11
+ */
12
+ export function extractIssueKeyFromBody(body: string): string | null {
13
+ const match = body.match(/<!-- issue-key: (.+?) -->/);
14
+ return match ? match[1] : null;
15
+ }
16
+
17
+ /**
18
+ * 判断评论是否为 AI 生成的评论(非用户真实回复)
19
+ * 除 issue-key 标记外,还通过结构化格式特征识别
20
+ */
21
+ export function isAiGeneratedComment(body: string): boolean {
22
+ if (!body) return false;
23
+ // 含 issue-key 标记
24
+ if (body.includes("<!-- issue-key:")) return true;
25
+ // 含 AI 评论的结构化格式特征(同时包含「规则」和「文件」字段)
26
+ if (body.includes("- **规则**:") && body.includes("- **文件**:")) return true;
27
+ return false;
28
+ }
29
+
30
+ export function generateIssueKey(issue: ReviewIssue): string {
31
+ return `${issue.file}:${issue.line}:${issue.ruleId}`;
32
+ }
33
+
34
+ /**
35
+ * 同步评论回复到对应的 issues
36
+ * review 评论回复是通过同一个 review 下的后续评论实现的
37
+ *
38
+ * 通过 AI 评论 body 中嵌入的 issue key 精确匹配 issue:
39
+ * - 含 issue key 的评论是 AI 自身评论,过滤掉不作为回复
40
+ * - 不含 issue key 但匹配 AI 格式特征的评论也视为 AI 评论,过滤掉
41
+ * - 其余评论是用户真实回复,归到其前面最近的 AI 评论对应的 issue
42
+ */
43
+ export async function syncRepliesToIssues(
44
+ reviewComments: {
45
+ id?: number;
46
+ path?: string;
47
+ position?: number;
48
+ body?: string;
49
+ user?: { id?: number; login?: string };
50
+ created_at?: string;
51
+ }[],
52
+ result: ReviewResult,
53
+ lineMatchesPosition: (issueLine: string, position?: number) => boolean,
54
+ ): Promise<void> {
55
+ try {
56
+ // 构建 issue key → issue 的映射,用于快速查找
57
+ const issueByKey = new Map<string, ReviewResult["issues"][0]>();
58
+ for (const issue of result.issues) {
59
+ issueByKey.set(generateIssueKey(issue), issue);
60
+ }
61
+ // 按文件路径和行号分组评论
62
+ const commentsByLocation = new Map<string, typeof reviewComments>();
63
+ for (const comment of reviewComments) {
64
+ if (!comment.path || !comment.position) continue;
65
+ const key = `${comment.path}:${comment.position}`;
66
+ const comments = commentsByLocation.get(key) || [];
67
+ comments.push(comment);
68
+ commentsByLocation.set(key, comments);
69
+ }
70
+ // 遍历每个位置的评论
71
+ for (const [, comments] of commentsByLocation) {
72
+ if (comments.length <= 1) continue;
73
+ // 按创建时间排序
74
+ comments.sort((a, b) => {
75
+ const timeA = a.created_at ? new Date(a.created_at).getTime() : 0;
76
+ const timeB = b.created_at ? new Date(b.created_at).getTime() : 0;
77
+ return timeA - timeB;
78
+ });
79
+ // 遍历评论,用 issue key 精确匹配
80
+ let lastIssueKey: string | null = null;
81
+ for (const comment of comments) {
82
+ const commentBody = comment.body || "";
83
+ const issueKey = extractIssueKeyFromBody(commentBody);
84
+ if (issueKey) {
85
+ // AI 自身评论(含 issue-key),记录 issue key 但不作为回复
86
+ lastIssueKey = issueKey;
87
+ continue;
88
+ }
89
+ // 跳过不含 issue-key 但匹配 AI 格式特征的评论(如其他轮次的 bot 评论)
90
+ if (isAiGeneratedComment(commentBody)) {
91
+ continue;
92
+ }
93
+ // 用户真实回复,通过前面最近的 AI 评论的 issue key 精确匹配
94
+ let matchedIssue = lastIssueKey ? (issueByKey.get(lastIssueKey) ?? null) : null;
95
+ // 回退:如果 issue key 匹配失败,使用 path:position 匹配
96
+ if (!matchedIssue) {
97
+ matchedIssue =
98
+ result.issues.find(
99
+ (issue) =>
100
+ issue.file === comment.path && lineMatchesPosition(issue.line, comment.position),
101
+ ) ?? null;
102
+ }
103
+ if (!matchedIssue) continue;
104
+ // 追加回复(而非覆盖,同一 issue 可能有多条用户回复)
105
+ if (!matchedIssue.replies) {
106
+ matchedIssue.replies = [];
107
+ }
108
+ matchedIssue.replies.push({
109
+ user: {
110
+ id: comment.user?.id?.toString(),
111
+ login: comment.user?.login || "unknown",
112
+ },
113
+ body: comment.body || "",
114
+ createdAt: comment.created_at || "",
115
+ });
116
+ }
117
+ }
118
+ } catch (error) {
119
+ console.warn("⚠️ 同步评论回复失败:", error);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 删除已有的 AI review(通过 marker 识别)
125
+ * - 删除行级评论的 PR Review(带 REVIEW_LINE_COMMENTS_MARKER)
126
+ * - 删除主评论的 Issue Comment(带 REVIEW_COMMENT_MARKER)
127
+ */
128
+ export async function deleteExistingAiReviews(pr: PullRequestModel): Promise<void> {
129
+ let deletedCount = 0;
130
+ // 删除行级评论的 PR Review
131
+ try {
132
+ const reviews = await pr.getReviews();
133
+ const aiReviews = reviews.filter(
134
+ (r) =>
135
+ r.body?.includes(REVIEW_LINE_COMMENTS_MARKER) || r.body?.includes(REVIEW_COMMENT_MARKER),
136
+ );
137
+ for (const review of aiReviews) {
138
+ if (review.id) {
139
+ try {
140
+ await pr.deleteReview(review.id);
141
+ deletedCount++;
142
+ } catch {
143
+ // 已提交的 review 无法删除,忽略
144
+ }
145
+ }
146
+ }
147
+ } catch (error) {
148
+ console.warn("⚠️ 列出 PR reviews 失败:", error);
149
+ }
150
+ // 删除主评论的 Issue Comment
151
+ try {
152
+ const comments = await pr.getComments();
153
+ const aiComments = comments.filter((c) => c.body?.includes(REVIEW_COMMENT_MARKER));
154
+ for (const comment of aiComments) {
155
+ if (comment.id) {
156
+ try {
157
+ await pr.deleteComment(comment.id);
158
+ deletedCount++;
159
+ } catch (error) {
160
+ console.warn(`⚠️ 删除评论 ${comment.id} 失败:`, error);
161
+ }
162
+ }
163
+ }
164
+ } catch (error) {
165
+ console.warn("⚠️ 列出 issue comments 失败:", error);
166
+ }
167
+ if (deletedCount > 0) {
168
+ console.log(`🗑️ 已删除 ${deletedCount} 个旧的 AI review`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * 计算问题状态统计
174
+ */
175
+ export function calculateIssueStats(issues: ReviewIssue[]): ReviewStats {
176
+ const total = issues.length;
177
+ const validIssue = issues.filter((i) => i.valid !== "false");
178
+ const validTotal = validIssue.length;
179
+ const fixed = validIssue.filter((i) => i.fixed).length;
180
+ const resolved = validIssue.filter((i) => i.resolved && !i.fixed).length;
181
+ const invalid = total - validTotal;
182
+ const pending = validTotal - fixed - resolved;
183
+ const fixRate = validTotal > 0 ? Math.round((fixed / validTotal) * 100 * 10) / 10 : 0;
184
+ const resolveRate = validTotal > 0 ? Math.round((resolved / validTotal) * 100 * 10) / 10 : 0;
185
+ return { total, validTotal, fixed, resolved, invalid, pending, fixRate, resolveRate };
186
+ }
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../../packages/core/tsconfig.skill.json",
2
+ "extends": "../../packages/core/tsconfig.extension.json",
3
3
  "compilerOptions": {
4
4
  "types": ["vitest/globals"]
5
5
  },