@spaceflow/review 5.0.0 → 5.0.2

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 (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -11
  3. package/dist/index.js +13 -44
  4. package/package.json +3 -3
  5. package/src/README.md +4 -4
  6. package/src/deletion-impact.service.spec.ts +8 -12
  7. package/src/deletion-impact.service.ts +5 -5
  8. package/src/issue-verify.service.spec.ts +0 -4
  9. package/src/locales/en/review.json +2 -2
  10. package/src/locales/zh-cn/review.json +2 -2
  11. package/src/mcp/index.ts +1 -5
  12. package/src/parse-title-options.spec.ts +4 -4
  13. package/src/parse-title-options.ts +3 -3
  14. package/src/review-context.ts +1 -1
  15. package/src/review-llm.spec.ts +1 -18
  16. package/src/review-result-model.ts +6 -6
  17. package/src/review.config.ts +2 -2
  18. package/src/review.service.spec.ts +4 -59
  19. package/src/review.service.ts +0 -27
  20. package/src/utils/review-pr-comment.spec.ts +8 -11
  21. package/src/utils/review-pr-comment.ts +2 -2
  22. package/src/coverage/base.css +0 -224
  23. package/src/coverage/block-navigation.js +0 -87
  24. package/src/coverage/clover.xml +0 -1942
  25. package/src/coverage/coverage-final.json +0 -7
  26. package/src/coverage/favicon.png +0 -0
  27. package/src/coverage/index.html +0 -131
  28. package/src/coverage/prettify.css +0 -1
  29. package/src/coverage/prettify.js +0 -2
  30. package/src/coverage/sort-arrow-sprite.png +0 -0
  31. package/src/coverage/sorter.js +0 -210
  32. package/src/coverage/src/deletion-impact.service.ts.html +0 -2716
  33. package/src/coverage/src/index.html +0 -161
  34. package/src/coverage/src/issue-verify.service.ts.html +0 -1006
  35. package/src/coverage/src/parse-title-options.ts.html +0 -640
  36. package/src/coverage/src/review-spec/index.html +0 -131
  37. package/src/coverage/src/review-spec/review-spec.service.ts.html +0 -2782
  38. package/src/coverage/src/review-spec/types.ts.html +0 -535
  39. package/src/coverage/src/review.service.ts.html +0 -8911
  40. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -29,9 +29,6 @@ function mockSummary(overrides: Partial<FileSummary> = {}): FileSummary {
29
29
  }
30
30
 
31
31
  vi.mock("c12");
32
- vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
33
- query: vi.fn(),
34
- }));
35
32
  vi.mock("fs/promises");
36
33
  vi.mock("child_process");
37
34
  vi.mock("@opencode-ai/sdk", () => ({
@@ -98,9 +95,6 @@ class TestReviewService extends ReviewService {
98
95
  executeDeletionOnly(context: Partial<ReviewContext>) {
99
96
  return super.executeDeletionOnly(context as ReviewContext);
100
97
  }
101
- ensureClaudeCli(ci?: boolean) {
102
- return super.ensureClaudeCli(ci);
103
- }
104
98
  resolveSourceData(context: Partial<ReviewContext>) {
105
99
  return super.resolveSourceData(context as ReviewContext);
106
100
  }
@@ -208,7 +202,7 @@ describe("ReviewService", () => {
208
202
  chat: vi.fn(),
209
203
  chatStream: vi.fn(),
210
204
  createSession: vi.fn(),
211
- getAvailableAdapters: vi.fn().mockReturnValue(["claude-code", "openai"]),
205
+ getAvailableAdapters: vi.fn().mockReturnValue(["openai", "open-code"]),
212
206
  };
213
207
 
214
208
  service = new TestReviewService(
@@ -292,7 +286,7 @@ describe("ReviewService", () => {
292
286
  specSources: ["/spec/dir"],
293
287
  dryRun: true,
294
288
  ci: false,
295
- llmMode: "claude-code" as const,
289
+ llmMode: "openai" as const,
296
290
  };
297
291
 
298
292
  const mockPR = {
@@ -358,7 +352,7 @@ describe("ReviewService", () => {
358
352
  specSources: ["/spec/dir"],
359
353
  dryRun: false,
360
354
  ci: true,
361
- llmMode: "claude-code" as const,
355
+ llmMode: "openai" as const,
362
356
  } as ReviewContext;
363
357
 
364
358
  const mockPR = {
@@ -408,7 +402,7 @@ describe("ReviewService", () => {
408
402
  specSources: ["/spec/dir"],
409
403
  dryRun: false,
410
404
  ci: false,
411
- llmMode: "claude-code",
405
+ llmMode: "openai",
412
406
  };
413
407
 
414
408
  gitProvider.getPullRequest.mockRejectedValue(new Error("Gitea API Error"));
@@ -922,13 +916,6 @@ describe("ReviewService", () => {
922
916
  });
923
917
  });
924
918
 
925
- describe("ReviewService.ensureClaudeCli", () => {
926
- it("should not throw when claude is installed", async () => {
927
- vi.spyOn(require("child_process"), "execSync").mockImplementation(() => Buffer.from("1.0.0"));
928
- await expect(service.ensureClaudeCli()).resolves.toBeUndefined();
929
- });
930
- });
931
-
932
919
  describe("ReviewService.resolveSourceData - direct file mode", () => {
933
920
  it("should bypass local uncommitted scanning when files are specified", async () => {
934
921
  const context: ReviewContext = {
@@ -1273,46 +1260,4 @@ describe("ReviewService", () => {
1273
1260
  expect(gitProvider.updateIssueComment).toHaveBeenCalled();
1274
1261
  });
1275
1262
  });
1276
-
1277
- describe("ReviewService.ensureClaudeCli", () => {
1278
- it("should do nothing when claude is already installed", async () => {
1279
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
1280
- // execSync is already mocked globally
1281
-
1282
- await service.ensureClaudeCli();
1283
- expect(consoleSpy).not.toHaveBeenCalledWith("🔧 Claude CLI 未安装,正在安装...");
1284
- consoleSpy.mockRestore();
1285
- });
1286
-
1287
- it("should install claude when not found", async () => {
1288
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
1289
- // Mock execSync to throw then succeed
1290
- const execSyncMock = vi.mocked(await import("child_process"));
1291
- execSyncMock.execSync
1292
- .mockImplementationOnce(() => {
1293
- throw new Error("command not found");
1294
- })
1295
- .mockImplementationOnce(() => Buffer.from(""));
1296
-
1297
- await service.ensureClaudeCli();
1298
- expect(consoleSpy).toHaveBeenCalledWith("🔧 Claude CLI 未安装,正在安装...");
1299
- expect(consoleSpy).toHaveBeenCalledWith("✅ Claude CLI 安装完成");
1300
- consoleSpy.mockRestore();
1301
- });
1302
-
1303
- it("should throw error when installation fails", async () => {
1304
- const execSyncMock = vi.mocked(await import("child_process"));
1305
- execSyncMock.execSync
1306
- .mockImplementationOnce(() => {
1307
- throw new Error("command not found");
1308
- })
1309
- .mockImplementationOnce(() => {
1310
- throw new Error("install failed");
1311
- });
1312
-
1313
- await expect(service.ensureClaudeCli()).rejects.toThrow(
1314
- "Claude CLI 安装失败: install failed",
1315
- );
1316
- });
1317
- });
1318
1263
  });
@@ -20,7 +20,6 @@ import { MarkdownFormatter, ReviewReportService } from "./review-report";
20
20
  import { ReviewOptions } from "./review.config";
21
21
  import { IssueVerifyService } from "./issue-verify.service";
22
22
  import { DeletionImpactService } from "./deletion-impact.service";
23
- import { execSync } from "child_process";
24
23
  import { ReviewContextBuilder, type ReviewContext } from "./review-context";
25
24
  import { ReviewIssueFilter } from "./review-issue-filter";
26
25
  import { filterFilesByIncludes } from "./review-includes-filter";
@@ -730,30 +729,4 @@ export class ReviewService {
730
729
 
731
730
  return result;
732
731
  }
733
-
734
- /**
735
- * 确保 Claude CLI 已安装
736
- */
737
- protected async ensureClaudeCli(ci?: boolean): Promise<void> {
738
- try {
739
- execSync("claude --version", { stdio: "ignore" });
740
- } catch {
741
- if (ci) {
742
- throw new Error(
743
- "Claude CLI 未安装。CI 环境请在 workflow 中预装: npm install -g @anthropic-ai/claude-code",
744
- );
745
- }
746
- console.log("🔧 Claude CLI 未安装,正在安装...");
747
- try {
748
- execSync("npm install -g @anthropic-ai/claude-code", {
749
- stdio: "inherit",
750
- });
751
- console.log("✅ Claude CLI 安装完成");
752
- } catch (installError) {
753
- throw new Error(
754
- `Claude CLI 安装失败: ${installError instanceof Error ? installError.message : String(installError)}`,
755
- );
756
- }
757
- }
758
- }
759
732
  }
@@ -27,9 +27,7 @@ describe("utils/review-pr-comment", () => {
27
27
 
28
28
  describe("extractIssueKeyFromBody", () => {
29
29
  it("提取标准格式的 issue key", () => {
30
- expect(extractIssueKeyFromBody("<!-- issue-key: src/a.ts:10:R1 -->")).toBe(
31
- "src/a.ts:10:R1",
32
- );
30
+ expect(extractIssueKeyFromBody("<!-- issue-key: src/a.ts:10:R1 -->")).toBe("src/a.ts:10:R1");
33
31
  });
34
32
 
35
33
  it("body 不含 issue-key 时返回 null", () => {
@@ -69,9 +67,9 @@ describe("utils/review-pr-comment", () => {
69
67
 
70
68
  describe("generateIssueKey", () => {
71
69
  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");
70
+ expect(generateIssueKey({ file: "src/a.ts", line: "10", ruleId: "R1" } as any)).toBe(
71
+ "src/a.ts:10:R1",
72
+ );
75
73
  });
76
74
  });
77
75
 
@@ -129,21 +127,20 @@ describe("utils/review-pr-comment", () => {
129
127
  });
130
128
 
131
129
  it("resolved(非 fixed)计入 resolved,不计入 pending", () => {
132
- const issues = [
133
- { file: "a.ts", line: "1", ruleId: "R1", resolved: "2024-01-01" },
134
- ] as any[];
130
+ const issues = [{ file: "a.ts", line: "1", ruleId: "R1", resolved: "2024-01-01" }] as any[];
135
131
  const stats = calculateIssueStats(issues);
136
132
  expect(stats.resolved).toBe(1);
137
133
  expect(stats.pending).toBe(0);
138
134
  });
139
135
 
140
- it("fixed 同时有 resolved 时只计入 fixed", () => {
136
+ it("fixed resolved 同时存在时各自独立计数,pending 不重复减", () => {
141
137
  const issues = [
142
138
  { file: "a.ts", line: "1", ruleId: "R1", fixed: "2024-01-01", resolved: "2024-01-01" },
143
139
  ] as any[];
144
140
  const stats = calculateIssueStats(issues);
145
141
  expect(stats.fixed).toBe(1);
146
- expect(stats.resolved).toBe(0);
142
+ expect(stats.resolved).toBe(1);
143
+ expect(stats.pending).toBe(0);
147
144
  });
148
145
 
149
146
  it("fixRate 计算正确(2/4 = 50%)", () => {
@@ -177,9 +177,9 @@ export function calculateIssueStats(issues: ReviewIssue[]): ReviewStats {
177
177
  const validIssue = issues.filter((i) => i.valid !== "false");
178
178
  const validTotal = validIssue.length;
179
179
  const fixed = validIssue.filter((i) => i.fixed).length;
180
- const resolved = validIssue.filter((i) => i.resolved && !i.fixed).length;
180
+ const resolved = validIssue.filter((i) => i.resolved).length;
181
181
  const invalid = total - validTotal;
182
- const pending = validTotal - fixed - resolved;
182
+ const pending = validTotal - validIssue.filter((i) => i.fixed || i.resolved).length;
183
183
  const fixRate = validTotal > 0 ? Math.round((fixed / validTotal) * 100 * 10) / 10 : 0;
184
184
  const resolveRate = validTotal > 0 ? Math.round((resolved / validTotal) * 100 * 10) / 10 : 0;
185
185
  return { total, validTotal, fixed, resolved, invalid, pending, fixRate, resolveRate };
@@ -1,224 +0,0 @@
1
- body, html {
2
- margin:0; padding: 0;
3
- height: 100%;
4
- }
5
- body {
6
- font-family: Helvetica Neue, Helvetica, Arial;
7
- font-size: 14px;
8
- color:#333;
9
- }
10
- .small { font-size: 12px; }
11
- *, *:after, *:before {
12
- -webkit-box-sizing:border-box;
13
- -moz-box-sizing:border-box;
14
- box-sizing:border-box;
15
- }
16
- h1 { font-size: 20px; margin: 0;}
17
- h2 { font-size: 14px; }
18
- pre {
19
- font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
20
- margin: 0;
21
- padding: 0;
22
- -moz-tab-size: 2;
23
- -o-tab-size: 2;
24
- tab-size: 2;
25
- }
26
- a { color:#0074D9; text-decoration:none; }
27
- a:hover { text-decoration:underline; }
28
- .strong { font-weight: bold; }
29
- .space-top1 { padding: 10px 0 0 0; }
30
- .pad2y { padding: 20px 0; }
31
- .pad1y { padding: 10px 0; }
32
- .pad2x { padding: 0 20px; }
33
- .pad2 { padding: 20px; }
34
- .pad1 { padding: 10px; }
35
- .space-left2 { padding-left:55px; }
36
- .space-right2 { padding-right:20px; }
37
- .center { text-align:center; }
38
- .clearfix { display:block; }
39
- .clearfix:after {
40
- content:'';
41
- display:block;
42
- height:0;
43
- clear:both;
44
- visibility:hidden;
45
- }
46
- .fl { float: left; }
47
- @media only screen and (max-width:640px) {
48
- .col3 { width:100%; max-width:100%; }
49
- .hide-mobile { display:none!important; }
50
- }
51
-
52
- .quiet {
53
- color: #7f7f7f;
54
- color: rgba(0,0,0,0.5);
55
- }
56
- .quiet a { opacity: 0.7; }
57
-
58
- .fraction {
59
- font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
60
- font-size: 10px;
61
- color: #555;
62
- background: #E8E8E8;
63
- padding: 4px 5px;
64
- border-radius: 3px;
65
- vertical-align: middle;
66
- }
67
-
68
- div.path a:link, div.path a:visited { color: #333; }
69
- table.coverage {
70
- border-collapse: collapse;
71
- margin: 10px 0 0 0;
72
- padding: 0;
73
- }
74
-
75
- table.coverage td {
76
- margin: 0;
77
- padding: 0;
78
- vertical-align: top;
79
- }
80
- table.coverage td.line-count {
81
- text-align: right;
82
- padding: 0 5px 0 20px;
83
- }
84
- table.coverage td.line-coverage {
85
- text-align: right;
86
- padding-right: 10px;
87
- min-width:20px;
88
- }
89
-
90
- table.coverage td span.cline-any {
91
- display: inline-block;
92
- padding: 0 5px;
93
- width: 100%;
94
- }
95
- .missing-if-branch {
96
- display: inline-block;
97
- margin-right: 5px;
98
- border-radius: 3px;
99
- position: relative;
100
- padding: 0 4px;
101
- background: #333;
102
- color: yellow;
103
- }
104
-
105
- .skip-if-branch {
106
- display: none;
107
- margin-right: 10px;
108
- position: relative;
109
- padding: 0 4px;
110
- background: #ccc;
111
- color: white;
112
- }
113
- .missing-if-branch .typ, .skip-if-branch .typ {
114
- color: inherit !important;
115
- }
116
- .coverage-summary {
117
- border-collapse: collapse;
118
- width: 100%;
119
- }
120
- .coverage-summary tr { border-bottom: 1px solid #bbb; }
121
- .keyline-all { border: 1px solid #ddd; }
122
- .coverage-summary td, .coverage-summary th { padding: 10px; }
123
- .coverage-summary tbody { border: 1px solid #bbb; }
124
- .coverage-summary td { border-right: 1px solid #bbb; }
125
- .coverage-summary td:last-child { border-right: none; }
126
- .coverage-summary th {
127
- text-align: left;
128
- font-weight: normal;
129
- white-space: nowrap;
130
- }
131
- .coverage-summary th.file { border-right: none !important; }
132
- .coverage-summary th.pct { }
133
- .coverage-summary th.pic,
134
- .coverage-summary th.abs,
135
- .coverage-summary td.pct,
136
- .coverage-summary td.abs { text-align: right; }
137
- .coverage-summary td.file { white-space: nowrap; }
138
- .coverage-summary td.pic { min-width: 120px !important; }
139
- .coverage-summary tfoot td { }
140
-
141
- .coverage-summary .sorter {
142
- height: 10px;
143
- width: 7px;
144
- display: inline-block;
145
- margin-left: 0.5em;
146
- background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
147
- }
148
- .coverage-summary .sorted .sorter {
149
- background-position: 0 -20px;
150
- }
151
- .coverage-summary .sorted-desc .sorter {
152
- background-position: 0 -10px;
153
- }
154
- .status-line { height: 10px; }
155
- /* yellow */
156
- .cbranch-no { background: yellow !important; color: #111; }
157
- /* dark red */
158
- .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
159
- .low .chart { border:1px solid #C21F39 }
160
- .highlighted,
161
- .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
162
- background: #C21F39 !important;
163
- }
164
- /* medium red */
165
- .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
166
- /* light red */
167
- .low, .cline-no { background:#FCE1E5 }
168
- /* light green */
169
- .high, .cline-yes { background:rgb(230,245,208) }
170
- /* medium green */
171
- .cstat-yes { background:rgb(161,215,106) }
172
- /* dark green */
173
- .status-line.high, .high .cover-fill { background:rgb(77,146,33) }
174
- .high .chart { border:1px solid rgb(77,146,33) }
175
- /* dark yellow (gold) */
176
- .status-line.medium, .medium .cover-fill { background: #f9cd0b; }
177
- .medium .chart { border:1px solid #f9cd0b; }
178
- /* light yellow */
179
- .medium { background: #fff4c2; }
180
-
181
- .cstat-skip { background: #ddd; color: #111; }
182
- .fstat-skip { background: #ddd; color: #111 !important; }
183
- .cbranch-skip { background: #ddd !important; color: #111; }
184
-
185
- span.cline-neutral { background: #eaeaea; }
186
-
187
- .coverage-summary td.empty {
188
- opacity: .5;
189
- padding-top: 4px;
190
- padding-bottom: 4px;
191
- line-height: 1;
192
- color: #888;
193
- }
194
-
195
- .cover-fill, .cover-empty {
196
- display:inline-block;
197
- height: 12px;
198
- }
199
- .chart {
200
- line-height: 0;
201
- }
202
- .cover-empty {
203
- background: white;
204
- }
205
- .cover-full {
206
- border-right: none !important;
207
- }
208
- pre.prettyprint {
209
- border: none !important;
210
- padding: 0 !important;
211
- margin: 0 !important;
212
- }
213
- .com { color: #999 !important; }
214
- .ignore-none { color: #999; font-weight: normal; }
215
-
216
- .wrapper {
217
- min-height: 100%;
218
- height: auto !important;
219
- height: 100%;
220
- margin: 0 auto -48px;
221
- }
222
- .footer, .push {
223
- height: 48px;
224
- }
@@ -1,87 +0,0 @@
1
- /* eslint-disable */
2
- var jumpToCode = (function init() {
3
- // Classes of code we would like to highlight in the file view
4
- var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
5
-
6
- // Elements to highlight in the file listing view
7
- var fileListingElements = ['td.pct.low'];
8
-
9
- // We don't want to select elements that are direct descendants of another match
10
- var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
11
-
12
- // Selector that finds elements on the page to which we can jump
13
- var selector =
14
- fileListingElements.join(', ') +
15
- ', ' +
16
- notSelector +
17
- missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
18
-
19
- // The NodeList of matching elements
20
- var missingCoverageElements = document.querySelectorAll(selector);
21
-
22
- var currentIndex;
23
-
24
- function toggleClass(index) {
25
- missingCoverageElements
26
- .item(currentIndex)
27
- .classList.remove('highlighted');
28
- missingCoverageElements.item(index).classList.add('highlighted');
29
- }
30
-
31
- function makeCurrent(index) {
32
- toggleClass(index);
33
- currentIndex = index;
34
- missingCoverageElements.item(index).scrollIntoView({
35
- behavior: 'smooth',
36
- block: 'center',
37
- inline: 'center'
38
- });
39
- }
40
-
41
- function goToPrevious() {
42
- var nextIndex = 0;
43
- if (typeof currentIndex !== 'number' || currentIndex === 0) {
44
- nextIndex = missingCoverageElements.length - 1;
45
- } else if (missingCoverageElements.length > 1) {
46
- nextIndex = currentIndex - 1;
47
- }
48
-
49
- makeCurrent(nextIndex);
50
- }
51
-
52
- function goToNext() {
53
- var nextIndex = 0;
54
-
55
- if (
56
- typeof currentIndex === 'number' &&
57
- currentIndex < missingCoverageElements.length - 1
58
- ) {
59
- nextIndex = currentIndex + 1;
60
- }
61
-
62
- makeCurrent(nextIndex);
63
- }
64
-
65
- return function jump(event) {
66
- if (
67
- document.getElementById('fileSearch') === document.activeElement &&
68
- document.activeElement != null
69
- ) {
70
- // if we're currently focused on the search input, we don't want to navigate
71
- return;
72
- }
73
-
74
- switch (event.which) {
75
- case 78: // n
76
- case 74: // j
77
- goToNext();
78
- break;
79
- case 66: // b
80
- case 75: // k
81
- case 80: // p
82
- goToPrevious();
83
- break;
84
- }
85
- };
86
- })();
87
- window.addEventListener('keydown', jumpToCode);