proof-pr 0.1.7 → 0.1.8
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/README.md +43 -17
- package/dist/index.js +619 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,41 +1,67 @@
|
|
|
1
1
|
# proof-pr
|
|
2
2
|
|
|
3
|
-
ProofPR
|
|
3
|
+
ProofPR 是给开源维护者和工程团队使用的 PR 证据门禁。它在投入深度 review 之前,先检查 PR 是否提供了足够证据:测试、复现、截图、changelog、权限理由,以及是否触碰敏感路径、依赖、workflow、MCP 或 secret 风险。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
它不依赖大模型,不上传代码,只基于 diff、PR 描述和配置做确定性判断。
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 快速使用
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
初始化配置和 GitHub Action:
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## 使用
|
|
11
|
+
```bash
|
|
12
|
+
npx proof-pr@latest init --preset open-source-maintainer
|
|
13
|
+
```
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
本地扫描当前分支:
|
|
17
16
|
|
|
18
17
|
```bash
|
|
19
|
-
npx proof-pr@latest init
|
|
20
|
-
npx proof-pr@latest init --preset security-strict
|
|
21
|
-
npx proof-pr@latest scan --base origin/main --head HEAD
|
|
22
18
|
npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN
|
|
23
|
-
npx proof-pr@latest scan --base origin/main --pr-body-file pr-body.md --format json
|
|
24
|
-
npx proof-pr@latest benchmark --cases benchmarks/cases
|
|
25
19
|
```
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
扫描内置案例:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx proof-pr@latest scan --diff-file examples/cases/workflow-untrusted-checkout.diff --locale zh-CN
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
生成独立 HTML 可视化报告:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN --format html > proofpr-report.html
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
运行 benchmark:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx proof-pr@latest benchmark --cases benchmarks/cases
|
|
37
|
+
```
|
|
28
38
|
|
|
29
39
|
## GitHub Action
|
|
30
40
|
|
|
31
41
|
```yaml
|
|
32
|
-
- uses: linsk27/proof-pr@v0.1.
|
|
42
|
+
- uses: linsk27/proof-pr@v0.1.8
|
|
33
43
|
with:
|
|
34
44
|
fail-on: high
|
|
35
45
|
comment: "true"
|
|
36
46
|
annotations: "true"
|
|
37
47
|
```
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
## 输出什么
|
|
50
|
+
|
|
51
|
+
- 风险等级:`low`、`medium`、`high`。
|
|
52
|
+
- 证据评分:0-100 分。
|
|
53
|
+
- Review 门禁:正常 review、重点 review、先补证据、风险处理前不要合并。
|
|
54
|
+
- Review 行动清单:维护者可直接执行的 checklist。
|
|
55
|
+
- 可选输出:GitHub annotations、SARIF、benchmark report、独立 HTML 可视化报告。
|
|
56
|
+
|
|
57
|
+
## 常用预设
|
|
58
|
+
|
|
59
|
+
- `open-source-maintainer`:开源仓库推荐。
|
|
60
|
+
- `security-strict`:安全敏感项目。
|
|
61
|
+
- `ai-generated-pr`:AI 生成 PR 较多的仓库。
|
|
62
|
+
- `mcp-security`:关注 MCP / agent 配置。
|
|
63
|
+
- `dependency-careful`:关注依赖和锁文件变化。
|
|
64
|
+
|
|
65
|
+
完整中文文档、截图和从 0 到 1 教程见仓库 README:
|
|
40
66
|
|
|
41
67
|
https://github.com/linsk27/proof-pr
|
package/dist/index.js
CHANGED
|
@@ -23371,6 +23371,434 @@ function renderMarkdownReport(result, locale = "en") {
|
|
|
23371
23371
|
}
|
|
23372
23372
|
return renderEnglishMarkdownReport(result);
|
|
23373
23373
|
}
|
|
23374
|
+
function renderHtmlReport(result, locale = "en") {
|
|
23375
|
+
const labels = htmlLabels(locale);
|
|
23376
|
+
const risk = locale === "zh-CN" ? translateRisk(result.risk) : result.risk;
|
|
23377
|
+
const decision = formatReviewDecision(result.reviewDecision, locale);
|
|
23378
|
+
const scoreGrade = formatEvidenceGrade(result.evidenceScore.grade, locale);
|
|
23379
|
+
const findingsBySeverity = countFindingsBySeverity(result.findings);
|
|
23380
|
+
const ruleCounts = countFindingsByRule(result.findings);
|
|
23381
|
+
const evidenceSignals = [
|
|
23382
|
+
[labels.prDescription, locale === "zh-CN" ? translateDescriptionState(result.summary.pullRequestDescription) : result.summary.pullRequestDescription, result.summary.pullRequestDescription === "present"],
|
|
23383
|
+
[labels.verification, yesNo(result.summary.verificationEvidence, locale), result.summary.verificationEvidence],
|
|
23384
|
+
[labels.reproduction, yesNo(result.summary.reproductionEvidence, locale), result.summary.reproductionEvidence],
|
|
23385
|
+
[labels.screenshot, yesNo(result.summary.screenshotEvidence, locale), result.summary.screenshotEvidence],
|
|
23386
|
+
[labels.changelog, yesNo(result.summary.changelogEvidence, locale), result.summary.changelogEvidence],
|
|
23387
|
+
[labels.permissionRationale, yesNo(result.summary.permissionRationaleEvidence, locale), result.summary.permissionRationaleEvidence]
|
|
23388
|
+
];
|
|
23389
|
+
return `<!doctype html>
|
|
23390
|
+
<html lang="${locale === "zh-CN" ? "zh-CN" : "en"}">
|
|
23391
|
+
<head>
|
|
23392
|
+
<meta charset="utf-8">
|
|
23393
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
23394
|
+
<title>ProofPR ${labels.report}</title>
|
|
23395
|
+
<style>
|
|
23396
|
+
:root {
|
|
23397
|
+
color-scheme: light;
|
|
23398
|
+
--bg: #f6f7f9;
|
|
23399
|
+
--panel: #ffffff;
|
|
23400
|
+
--ink: #17202a;
|
|
23401
|
+
--muted: #667085;
|
|
23402
|
+
--line: #d9dee7;
|
|
23403
|
+
--green: #138a5e;
|
|
23404
|
+
--amber: #b7791f;
|
|
23405
|
+
--red: #c24135;
|
|
23406
|
+
--blue: #2563a9;
|
|
23407
|
+
--soft-green: #e8f6ef;
|
|
23408
|
+
--soft-amber: #fff3d6;
|
|
23409
|
+
--soft-red: #fdebea;
|
|
23410
|
+
--soft-blue: #eaf2fb;
|
|
23411
|
+
}
|
|
23412
|
+
|
|
23413
|
+
* { box-sizing: border-box; }
|
|
23414
|
+
|
|
23415
|
+
body {
|
|
23416
|
+
margin: 0;
|
|
23417
|
+
background: var(--bg);
|
|
23418
|
+
color: var(--ink);
|
|
23419
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
23420
|
+
line-height: 1.5;
|
|
23421
|
+
}
|
|
23422
|
+
|
|
23423
|
+
main {
|
|
23424
|
+
width: min(1180px, calc(100vw - 32px));
|
|
23425
|
+
margin: 0 auto;
|
|
23426
|
+
padding: 32px 0 48px;
|
|
23427
|
+
}
|
|
23428
|
+
|
|
23429
|
+
.topbar {
|
|
23430
|
+
display: flex;
|
|
23431
|
+
justify-content: space-between;
|
|
23432
|
+
gap: 18px;
|
|
23433
|
+
align-items: flex-start;
|
|
23434
|
+
margin-bottom: 20px;
|
|
23435
|
+
}
|
|
23436
|
+
|
|
23437
|
+
h1, h2, h3, p { margin: 0; }
|
|
23438
|
+
|
|
23439
|
+
h1 {
|
|
23440
|
+
font-size: 28px;
|
|
23441
|
+
line-height: 1.2;
|
|
23442
|
+
}
|
|
23443
|
+
|
|
23444
|
+
h2 {
|
|
23445
|
+
font-size: 17px;
|
|
23446
|
+
margin-bottom: 14px;
|
|
23447
|
+
}
|
|
23448
|
+
|
|
23449
|
+
h3 {
|
|
23450
|
+
font-size: 15px;
|
|
23451
|
+
margin-bottom: 8px;
|
|
23452
|
+
}
|
|
23453
|
+
|
|
23454
|
+
.subtitle {
|
|
23455
|
+
color: var(--muted);
|
|
23456
|
+
margin-top: 8px;
|
|
23457
|
+
max-width: 760px;
|
|
23458
|
+
}
|
|
23459
|
+
|
|
23460
|
+
.pill {
|
|
23461
|
+
display: inline-flex;
|
|
23462
|
+
align-items: center;
|
|
23463
|
+
border: 1px solid var(--line);
|
|
23464
|
+
border-radius: 999px;
|
|
23465
|
+
padding: 5px 10px;
|
|
23466
|
+
background: var(--panel);
|
|
23467
|
+
color: var(--muted);
|
|
23468
|
+
font-size: 13px;
|
|
23469
|
+
white-space: nowrap;
|
|
23470
|
+
}
|
|
23471
|
+
|
|
23472
|
+
.grid {
|
|
23473
|
+
display: grid;
|
|
23474
|
+
grid-template-columns: repeat(12, 1fr);
|
|
23475
|
+
gap: 14px;
|
|
23476
|
+
}
|
|
23477
|
+
|
|
23478
|
+
.card {
|
|
23479
|
+
background: var(--panel);
|
|
23480
|
+
border: 1px solid var(--line);
|
|
23481
|
+
border-radius: 8px;
|
|
23482
|
+
padding: 18px;
|
|
23483
|
+
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
|
23484
|
+
}
|
|
23485
|
+
|
|
23486
|
+
.metric { grid-column: span 3; }
|
|
23487
|
+
.wide { grid-column: span 8; }
|
|
23488
|
+
.side { grid-column: span 4; }
|
|
23489
|
+
.full { grid-column: 1 / -1; }
|
|
23490
|
+
|
|
23491
|
+
.metric-label {
|
|
23492
|
+
color: var(--muted);
|
|
23493
|
+
font-size: 13px;
|
|
23494
|
+
margin-bottom: 8px;
|
|
23495
|
+
}
|
|
23496
|
+
|
|
23497
|
+
.metric-value {
|
|
23498
|
+
font-size: 27px;
|
|
23499
|
+
font-weight: 720;
|
|
23500
|
+
line-height: 1.1;
|
|
23501
|
+
}
|
|
23502
|
+
|
|
23503
|
+
.tone-low { color: var(--green); background: var(--soft-green); border-color: #b8e5cf; }
|
|
23504
|
+
.tone-medium { color: var(--amber); background: var(--soft-amber); border-color: #f1d28a; }
|
|
23505
|
+
.tone-high { color: var(--red); background: var(--soft-red); border-color: #f3b6b1; }
|
|
23506
|
+
|
|
23507
|
+
.scorebar {
|
|
23508
|
+
width: 100%;
|
|
23509
|
+
height: 16px;
|
|
23510
|
+
border: 1px solid var(--line);
|
|
23511
|
+
border-radius: 999px;
|
|
23512
|
+
overflow: hidden;
|
|
23513
|
+
margin: 14px 0 10px;
|
|
23514
|
+
background: #eef1f5;
|
|
23515
|
+
}
|
|
23516
|
+
|
|
23517
|
+
.scorefill {
|
|
23518
|
+
height: 100%;
|
|
23519
|
+
width: ${result.evidenceScore.value}%;
|
|
23520
|
+
background: ${scoreColor(result.evidenceScore.value)};
|
|
23521
|
+
}
|
|
23522
|
+
|
|
23523
|
+
.summary-grid {
|
|
23524
|
+
display: grid;
|
|
23525
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
23526
|
+
gap: 10px;
|
|
23527
|
+
}
|
|
23528
|
+
|
|
23529
|
+
.summary-item {
|
|
23530
|
+
border: 1px solid var(--line);
|
|
23531
|
+
border-radius: 8px;
|
|
23532
|
+
padding: 10px 12px;
|
|
23533
|
+
background: #fbfcfd;
|
|
23534
|
+
}
|
|
23535
|
+
|
|
23536
|
+
.summary-item strong {
|
|
23537
|
+
display: block;
|
|
23538
|
+
font-size: 20px;
|
|
23539
|
+
margin-bottom: 2px;
|
|
23540
|
+
}
|
|
23541
|
+
|
|
23542
|
+
.summary-item span {
|
|
23543
|
+
color: var(--muted);
|
|
23544
|
+
font-size: 12px;
|
|
23545
|
+
}
|
|
23546
|
+
|
|
23547
|
+
.signal-list, .action-list, .finding-list, .focus-list, .deduction-list, .rule-list {
|
|
23548
|
+
display: grid;
|
|
23549
|
+
gap: 10px;
|
|
23550
|
+
}
|
|
23551
|
+
|
|
23552
|
+
.signal, .action, .focus, .deduction, .rule-row {
|
|
23553
|
+
border: 1px solid var(--line);
|
|
23554
|
+
border-radius: 8px;
|
|
23555
|
+
padding: 10px 12px;
|
|
23556
|
+
background: #fbfcfd;
|
|
23557
|
+
}
|
|
23558
|
+
|
|
23559
|
+
.signal {
|
|
23560
|
+
display: flex;
|
|
23561
|
+
justify-content: space-between;
|
|
23562
|
+
gap: 12px;
|
|
23563
|
+
align-items: center;
|
|
23564
|
+
}
|
|
23565
|
+
|
|
23566
|
+
.signal-name, .action-title, .finding-title {
|
|
23567
|
+
font-weight: 680;
|
|
23568
|
+
}
|
|
23569
|
+
|
|
23570
|
+
.signal-state {
|
|
23571
|
+
font-size: 12px;
|
|
23572
|
+
border-radius: 999px;
|
|
23573
|
+
padding: 3px 8px;
|
|
23574
|
+
border: 1px solid var(--line);
|
|
23575
|
+
white-space: nowrap;
|
|
23576
|
+
}
|
|
23577
|
+
|
|
23578
|
+
.severity-grid {
|
|
23579
|
+
display: grid;
|
|
23580
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
23581
|
+
gap: 8px;
|
|
23582
|
+
}
|
|
23583
|
+
|
|
23584
|
+
.severity {
|
|
23585
|
+
border: 1px solid var(--line);
|
|
23586
|
+
border-radius: 8px;
|
|
23587
|
+
padding: 10px;
|
|
23588
|
+
background: #fbfcfd;
|
|
23589
|
+
}
|
|
23590
|
+
|
|
23591
|
+
.severity strong {
|
|
23592
|
+
display: block;
|
|
23593
|
+
font-size: 22px;
|
|
23594
|
+
}
|
|
23595
|
+
|
|
23596
|
+
.muted {
|
|
23597
|
+
color: var(--muted);
|
|
23598
|
+
font-size: 13px;
|
|
23599
|
+
}
|
|
23600
|
+
|
|
23601
|
+
.action {
|
|
23602
|
+
display: grid;
|
|
23603
|
+
grid-template-columns: auto 1fr;
|
|
23604
|
+
gap: 10px;
|
|
23605
|
+
}
|
|
23606
|
+
|
|
23607
|
+
.box {
|
|
23608
|
+
width: 18px;
|
|
23609
|
+
height: 18px;
|
|
23610
|
+
border: 2px solid var(--blue);
|
|
23611
|
+
border-radius: 4px;
|
|
23612
|
+
margin-top: 2px;
|
|
23613
|
+
}
|
|
23614
|
+
|
|
23615
|
+
.priority {
|
|
23616
|
+
display: inline-flex;
|
|
23617
|
+
margin-left: 6px;
|
|
23618
|
+
color: var(--muted);
|
|
23619
|
+
font-size: 12px;
|
|
23620
|
+
font-weight: 560;
|
|
23621
|
+
}
|
|
23622
|
+
|
|
23623
|
+
.finding {
|
|
23624
|
+
border: 1px solid var(--line);
|
|
23625
|
+
border-radius: 8px;
|
|
23626
|
+
padding: 14px;
|
|
23627
|
+
background: #fff;
|
|
23628
|
+
}
|
|
23629
|
+
|
|
23630
|
+
.finding-head {
|
|
23631
|
+
display: flex;
|
|
23632
|
+
justify-content: space-between;
|
|
23633
|
+
gap: 12px;
|
|
23634
|
+
align-items: flex-start;
|
|
23635
|
+
margin-bottom: 8px;
|
|
23636
|
+
}
|
|
23637
|
+
|
|
23638
|
+
code {
|
|
23639
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
23640
|
+
font-size: 12px;
|
|
23641
|
+
background: #f0f3f7;
|
|
23642
|
+
border: 1px solid var(--line);
|
|
23643
|
+
border-radius: 6px;
|
|
23644
|
+
padding: 2px 5px;
|
|
23645
|
+
word-break: break-word;
|
|
23646
|
+
}
|
|
23647
|
+
|
|
23648
|
+
.evidence-list {
|
|
23649
|
+
margin: 10px 0 0;
|
|
23650
|
+
padding-left: 18px;
|
|
23651
|
+
color: var(--muted);
|
|
23652
|
+
}
|
|
23653
|
+
|
|
23654
|
+
.footer {
|
|
23655
|
+
color: var(--muted);
|
|
23656
|
+
font-size: 12px;
|
|
23657
|
+
margin-top: 18px;
|
|
23658
|
+
text-align: center;
|
|
23659
|
+
}
|
|
23660
|
+
|
|
23661
|
+
@media (max-width: 860px) {
|
|
23662
|
+
main { width: min(100vw - 20px, 1180px); padding-top: 20px; }
|
|
23663
|
+
.topbar { display: block; }
|
|
23664
|
+
.pill { margin-top: 12px; }
|
|
23665
|
+
.metric, .wide, .side { grid-column: 1 / -1; }
|
|
23666
|
+
.summary-grid, .severity-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
23667
|
+
}
|
|
23668
|
+
</style>
|
|
23669
|
+
</head>
|
|
23670
|
+
<body>
|
|
23671
|
+
<main>
|
|
23672
|
+
<section class="topbar">
|
|
23673
|
+
<div>
|
|
23674
|
+
<h1>ProofPR ${labels.report}</h1>
|
|
23675
|
+
<p class="subtitle">${labels.subtitle}</p>
|
|
23676
|
+
</div>
|
|
23677
|
+
<span class="pill">${labels.generated}</span>
|
|
23678
|
+
</section>
|
|
23679
|
+
|
|
23680
|
+
<section class="grid">
|
|
23681
|
+
<article class="card metric">
|
|
23682
|
+
<div class="metric-label">${labels.risk}</div>
|
|
23683
|
+
<div class="metric-value">${escapeHtml(risk)}</div>
|
|
23684
|
+
<span class="pill tone-${result.risk}">${escapeHtml(result.risk)}</span>
|
|
23685
|
+
</article>
|
|
23686
|
+
<article class="card metric">
|
|
23687
|
+
<div class="metric-label">${labels.evidenceScore}</div>
|
|
23688
|
+
<div class="metric-value">${result.evidenceScore.value}/100</div>
|
|
23689
|
+
<div class="scorebar" aria-label="${labels.evidenceScore}">
|
|
23690
|
+
<div class="scorefill"></div>
|
|
23691
|
+
</div>
|
|
23692
|
+
<div class="muted">${escapeHtml(scoreGrade)}</div>
|
|
23693
|
+
</article>
|
|
23694
|
+
<article class="card metric">
|
|
23695
|
+
<div class="metric-label">${labels.reviewGate}</div>
|
|
23696
|
+
<div class="metric-value" style="font-size: 20px;">${escapeHtml(decision)}</div>
|
|
23697
|
+
</article>
|
|
23698
|
+
<article class="card metric">
|
|
23699
|
+
<div class="metric-label">${labels.findings}</div>
|
|
23700
|
+
<div class="metric-value">${result.findings.length}</div>
|
|
23701
|
+
<div class="muted">${labels.findingsHint}</div>
|
|
23702
|
+
</article>
|
|
23703
|
+
|
|
23704
|
+
<article class="card wide">
|
|
23705
|
+
<h2>${labels.changeSummary}</h2>
|
|
23706
|
+
<div class="summary-grid">
|
|
23707
|
+
${summaryItem(labels.filesChanged, result.summary.filesChanged)}
|
|
23708
|
+
${summaryItem(labels.additions, result.summary.additions)}
|
|
23709
|
+
${summaryItem(labels.deletions, result.summary.deletions)}
|
|
23710
|
+
${summaryItem(labels.sensitiveFiles, result.summary.sensitiveFilesChanged)}
|
|
23711
|
+
${summaryItem(labels.testFiles, result.summary.testFilesChanged)}
|
|
23712
|
+
${summaryItem(labels.highFindings, findingsBySeverity.high)}
|
|
23713
|
+
${summaryItem(labels.mediumFindings, findingsBySeverity.medium)}
|
|
23714
|
+
${summaryItem(labels.lowFindings, findingsBySeverity.low)}
|
|
23715
|
+
</div>
|
|
23716
|
+
</article>
|
|
23717
|
+
|
|
23718
|
+
<article class="card side">
|
|
23719
|
+
<h2>${labels.evidenceSignals}</h2>
|
|
23720
|
+
<div class="signal-list">
|
|
23721
|
+
${evidenceSignals.map(([name, state, ok]) => signalItem(name, state, ok)).join("\n")}
|
|
23722
|
+
</div>
|
|
23723
|
+
</article>
|
|
23724
|
+
|
|
23725
|
+
<article class="card wide">
|
|
23726
|
+
<h2>${labels.reviewPlan}</h2>
|
|
23727
|
+
<div class="action-list">
|
|
23728
|
+
${result.reviewPlan.actionItems.length > 0
|
|
23729
|
+
? result.reviewPlan.actionItems.map((action) => `
|
|
23730
|
+
<div class="action">
|
|
23731
|
+
<span class="box"></span>
|
|
23732
|
+
<div>
|
|
23733
|
+
<div class="action-title">${escapeHtml(localizeActionTitle(action.actionId, action.title, locale))}<span class="priority">${escapeHtml(formatPriority(action.priority, locale))}</span></div>
|
|
23734
|
+
<div class="muted">${escapeHtml(localizeActionDetail(action.actionId, action.detail, locale))}</div>
|
|
23735
|
+
</div>
|
|
23736
|
+
</div>`).join("\n")
|
|
23737
|
+
: `<div class="muted">${labels.noActions}</div>`}
|
|
23738
|
+
</div>
|
|
23739
|
+
</article>
|
|
23740
|
+
|
|
23741
|
+
<article class="card side">
|
|
23742
|
+
<h2>${labels.findingDistribution}</h2>
|
|
23743
|
+
<div class="severity-grid">
|
|
23744
|
+
${severityItem("high", findingsBySeverity.high, labels.high)}
|
|
23745
|
+
${severityItem("medium", findingsBySeverity.medium, labels.medium)}
|
|
23746
|
+
${severityItem("low", findingsBySeverity.low, labels.low)}
|
|
23747
|
+
${severityItem("info", findingsBySeverity.info, labels.info)}
|
|
23748
|
+
</div>
|
|
23749
|
+
</article>
|
|
23750
|
+
|
|
23751
|
+
<article class="card side">
|
|
23752
|
+
<h2>${labels.focusFiles}</h2>
|
|
23753
|
+
<div class="focus-list">
|
|
23754
|
+
${result.reviewPlan.focusFiles.length > 0
|
|
23755
|
+
? result.reviewPlan.focusFiles.map((file) => `
|
|
23756
|
+
<div class="focus">
|
|
23757
|
+
<div><code>${escapeHtml(file.path)}</code></div>
|
|
23758
|
+
<div class="muted">${escapeHtml(localizeFocusReason(file.reasonId, file.reason, locale))}</div>
|
|
23759
|
+
</div>`).join("\n")
|
|
23760
|
+
: `<div class="muted">${labels.noFocusFiles}</div>`}
|
|
23761
|
+
</div>
|
|
23762
|
+
</article>
|
|
23763
|
+
|
|
23764
|
+
<article class="card side">
|
|
23765
|
+
<h2>${labels.scoreDetails}</h2>
|
|
23766
|
+
<div class="deduction-list">
|
|
23767
|
+
${result.evidenceScore.deductions.length > 0
|
|
23768
|
+
? result.evidenceScore.deductions.map((deduction) => `
|
|
23769
|
+
<div class="deduction">
|
|
23770
|
+
<strong>-${deduction.points}</strong>
|
|
23771
|
+
<div class="muted">${escapeHtml(localizeDeduction(deduction.reasonId, deduction.message, locale))}</div>
|
|
23772
|
+
</div>`).join("\n")
|
|
23773
|
+
: `<div class="muted">${labels.noDeductions}</div>`}
|
|
23774
|
+
</div>
|
|
23775
|
+
</article>
|
|
23776
|
+
|
|
23777
|
+
<article class="card full">
|
|
23778
|
+
<h2>${labels.rulesCovered}</h2>
|
|
23779
|
+
<div class="rule-list">
|
|
23780
|
+
${ruleCounts.length > 0
|
|
23781
|
+
? ruleCounts.map((item) => `<div class="rule-row"><code>${escapeHtml(item.ruleId)}</code> <span class="muted">${item.count}</span></div>`).join("\n")
|
|
23782
|
+
: `<div class="muted">${labels.noRules}</div>`}
|
|
23783
|
+
</div>
|
|
23784
|
+
</article>
|
|
23785
|
+
|
|
23786
|
+
<article class="card full">
|
|
23787
|
+
<h2>${labels.findings}</h2>
|
|
23788
|
+
<div class="finding-list">
|
|
23789
|
+
${result.findings.length > 0
|
|
23790
|
+
? result.findings.map((finding) => htmlFinding(finding, locale)).join("\n")
|
|
23791
|
+
: `<div class="muted">${labels.noFindings}</div>`}
|
|
23792
|
+
</div>
|
|
23793
|
+
</article>
|
|
23794
|
+
</section>
|
|
23795
|
+
|
|
23796
|
+
<p class="footer">${labels.footer}</p>
|
|
23797
|
+
</main>
|
|
23798
|
+
</body>
|
|
23799
|
+
</html>
|
|
23800
|
+
`;
|
|
23801
|
+
}
|
|
23374
23802
|
function getReportMarker() {
|
|
23375
23803
|
return REPORT_MARKER;
|
|
23376
23804
|
}
|
|
@@ -23906,6 +24334,191 @@ function translateDeduction(reasonId, fallback) {
|
|
|
23906
24334
|
"missing-tests": "代码发生变更,但缺少测试变更或验证说明。"
|
|
23907
24335
|
}[reasonId] ?? fallback;
|
|
23908
24336
|
}
|
|
24337
|
+
function htmlLabels(locale) {
|
|
24338
|
+
if (locale === "zh-CN") {
|
|
24339
|
+
return {
|
|
24340
|
+
report: "可视化报告",
|
|
24341
|
+
subtitle: "把 PR 风险、证据质量、Review 门禁和维护者行动清单整理成一个可分享的静态页面。",
|
|
24342
|
+
generated: "Generated by ProofPR",
|
|
24343
|
+
risk: "风险等级",
|
|
24344
|
+
evidenceScore: "证据评分",
|
|
24345
|
+
reviewGate: "Review 门禁",
|
|
24346
|
+
findings: "风险发现",
|
|
24347
|
+
findingsHint: "需要维护者优先关注的信号",
|
|
24348
|
+
changeSummary: "改动概览",
|
|
24349
|
+
filesChanged: "改动文件",
|
|
24350
|
+
additions: "新增行",
|
|
24351
|
+
deletions: "删除行",
|
|
24352
|
+
sensitiveFiles: "敏感文件",
|
|
24353
|
+
testFiles: "测试文件",
|
|
24354
|
+
highFindings: "高风险",
|
|
24355
|
+
mediumFindings: "中风险",
|
|
24356
|
+
lowFindings: "低风险",
|
|
24357
|
+
evidenceSignals: "证据信号",
|
|
24358
|
+
prDescription: "PR 描述",
|
|
24359
|
+
verification: "验证证据",
|
|
24360
|
+
reproduction: "复现上下文",
|
|
24361
|
+
screenshot: "截图证据",
|
|
24362
|
+
changelog: "Changelog",
|
|
24363
|
+
permissionRationale: "权限理由",
|
|
24364
|
+
reviewPlan: "Review 行动清单",
|
|
24365
|
+
noActions: "没有额外行动项。",
|
|
24366
|
+
findingDistribution: "Finding 分布",
|
|
24367
|
+
high: "高",
|
|
24368
|
+
medium: "中",
|
|
24369
|
+
low: "低",
|
|
24370
|
+
info: "信息",
|
|
24371
|
+
focusFiles: "重点文件",
|
|
24372
|
+
noFocusFiles: "没有重点文件。",
|
|
24373
|
+
scoreDetails: "证据扣分",
|
|
24374
|
+
noDeductions: "没有扣分项。",
|
|
24375
|
+
rulesCovered: "命中规则",
|
|
24376
|
+
noRules: "没有规则命中。",
|
|
24377
|
+
noFindings: "启用的规则没有发现需要优先关注的 review 风险。",
|
|
24378
|
+
rule: "规则",
|
|
24379
|
+
severity: "严重程度",
|
|
24380
|
+
path: "路径",
|
|
24381
|
+
detail: "详情",
|
|
24382
|
+
evidence: "证据",
|
|
24383
|
+
recommendation: "建议",
|
|
24384
|
+
footer: "ProofPR 不替代人工 review,它帮助维护者先判断证据是否足够、风险边界是否清楚。"
|
|
24385
|
+
};
|
|
24386
|
+
}
|
|
24387
|
+
return {
|
|
24388
|
+
report: "Visual Report",
|
|
24389
|
+
subtitle: "A shareable static view of PR risk, evidence quality, review gate, and maintainer actions.",
|
|
24390
|
+
generated: "Generated by ProofPR",
|
|
24391
|
+
risk: "Risk",
|
|
24392
|
+
evidenceScore: "Evidence score",
|
|
24393
|
+
reviewGate: "Review gate",
|
|
24394
|
+
findings: "Findings",
|
|
24395
|
+
findingsHint: "Signals that deserve maintainer attention",
|
|
24396
|
+
changeSummary: "Change summary",
|
|
24397
|
+
filesChanged: "Files changed",
|
|
24398
|
+
additions: "Additions",
|
|
24399
|
+
deletions: "Deletions",
|
|
24400
|
+
sensitiveFiles: "Sensitive files",
|
|
24401
|
+
testFiles: "Test files",
|
|
24402
|
+
highFindings: "High findings",
|
|
24403
|
+
mediumFindings: "Medium findings",
|
|
24404
|
+
lowFindings: "Low findings",
|
|
24405
|
+
evidenceSignals: "Evidence signals",
|
|
24406
|
+
prDescription: "PR description",
|
|
24407
|
+
verification: "Verification",
|
|
24408
|
+
reproduction: "Reproduction",
|
|
24409
|
+
screenshot: "Screenshot",
|
|
24410
|
+
changelog: "Changelog",
|
|
24411
|
+
permissionRationale: "Permission rationale",
|
|
24412
|
+
reviewPlan: "Review plan",
|
|
24413
|
+
noActions: "No additional action items.",
|
|
24414
|
+
findingDistribution: "Finding distribution",
|
|
24415
|
+
high: "High",
|
|
24416
|
+
medium: "Medium",
|
|
24417
|
+
low: "Low",
|
|
24418
|
+
info: "Info",
|
|
24419
|
+
focusFiles: "Focus files",
|
|
24420
|
+
noFocusFiles: "No focus files.",
|
|
24421
|
+
scoreDetails: "Evidence deductions",
|
|
24422
|
+
noDeductions: "No deductions.",
|
|
24423
|
+
rulesCovered: "Rules covered",
|
|
24424
|
+
noRules: "No rule hits.",
|
|
24425
|
+
noFindings: "No review-risk findings detected by the enabled rules.",
|
|
24426
|
+
rule: "Rule",
|
|
24427
|
+
severity: "Severity",
|
|
24428
|
+
path: "Path",
|
|
24429
|
+
detail: "Detail",
|
|
24430
|
+
evidence: "Evidence",
|
|
24431
|
+
recommendation: "Recommendation",
|
|
24432
|
+
footer: "ProofPR does not replace human review. It helps maintainers decide whether evidence is enough and risk boundaries are clear."
|
|
24433
|
+
};
|
|
24434
|
+
}
|
|
24435
|
+
function summaryItem(label, value) {
|
|
24436
|
+
return `<div class="summary-item"><strong>${value}</strong><span>${escapeHtml(label)}</span></div>`;
|
|
24437
|
+
}
|
|
24438
|
+
function signalItem(name, state, ok) {
|
|
24439
|
+
return `<div class="signal"><span class="signal-name">${escapeHtml(name)}</span><span class="signal-state ${ok ? "tone-low" : "tone-medium"}">${escapeHtml(state)}</span></div>`;
|
|
24440
|
+
}
|
|
24441
|
+
function severityItem(severity, value, label) {
|
|
24442
|
+
return `<div class="severity ${severity === "high" ? "tone-high" : severity === "medium" ? "tone-medium" : severity === "low" ? "tone-low" : ""}"><strong>${value}</strong><span>${escapeHtml(label)}</span></div>`;
|
|
24443
|
+
}
|
|
24444
|
+
function htmlFinding(finding, locale) {
|
|
24445
|
+
const labels = htmlLabels(locale);
|
|
24446
|
+
const translated = locale === "zh-CN" ? translateFinding(finding) : finding;
|
|
24447
|
+
const evidence = finding.evidence && finding.evidence.length > 0
|
|
24448
|
+
? `<ul class="evidence-list">${finding.evidence
|
|
24449
|
+
.map((item) => `<li><code>${escapeHtml(locale === "zh-CN" ? translateEvidence(item) : item)}</code></li>`)
|
|
24450
|
+
.join("")}</ul>`
|
|
24451
|
+
: "";
|
|
24452
|
+
const path = finding.path
|
|
24453
|
+
? `<div class="muted">${labels.path}: <code>${escapeHtml(finding.path)}</code></div>`
|
|
24454
|
+
: "";
|
|
24455
|
+
const recommendation = translated.recommendation
|
|
24456
|
+
? `<div class="muted">${labels.recommendation}: ${escapeHtml(translated.recommendation)}</div>`
|
|
24457
|
+
: "";
|
|
24458
|
+
return `<div class="finding">
|
|
24459
|
+
<div class="finding-head">
|
|
24460
|
+
<div>
|
|
24461
|
+
<div class="finding-title">${escapeHtml(translated.title)}</div>
|
|
24462
|
+
<div class="muted">${labels.rule}: <code>${escapeHtml(finding.ruleId)}</code></div>
|
|
24463
|
+
</div>
|
|
24464
|
+
<span class="pill ${finding.severity === "high" ? "tone-high" : finding.severity === "medium" ? "tone-medium" : "tone-low"}">${escapeHtml(locale === "zh-CN" ? translateSeverity(finding.severity) : finding.severity)}</span>
|
|
24465
|
+
</div>
|
|
24466
|
+
${path}
|
|
24467
|
+
<div class="muted">${labels.detail}: ${escapeHtml(translated.message)}</div>
|
|
24468
|
+
${evidence}
|
|
24469
|
+
${recommendation}
|
|
24470
|
+
</div>`;
|
|
24471
|
+
}
|
|
24472
|
+
function countFindingsBySeverity(findings) {
|
|
24473
|
+
return findings.reduce((counts, finding) => {
|
|
24474
|
+
counts[finding.severity] += 1;
|
|
24475
|
+
return counts;
|
|
24476
|
+
}, { info: 0, low: 0, medium: 0, high: 0 });
|
|
24477
|
+
}
|
|
24478
|
+
function countFindingsByRule(findings) {
|
|
24479
|
+
const counts = new Map();
|
|
24480
|
+
for (const finding of findings) {
|
|
24481
|
+
counts.set(finding.ruleId, (counts.get(finding.ruleId) ?? 0) + 1);
|
|
24482
|
+
}
|
|
24483
|
+
return [...counts.entries()]
|
|
24484
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
24485
|
+
.map(([ruleId, count]) => ({ ruleId, count }));
|
|
24486
|
+
}
|
|
24487
|
+
function scoreColor(value) {
|
|
24488
|
+
if (value >= 85) {
|
|
24489
|
+
return "var(--green)";
|
|
24490
|
+
}
|
|
24491
|
+
if (value >= 70) {
|
|
24492
|
+
return "var(--blue)";
|
|
24493
|
+
}
|
|
24494
|
+
if (value >= 50) {
|
|
24495
|
+
return "var(--amber)";
|
|
24496
|
+
}
|
|
24497
|
+
return "var(--red)";
|
|
24498
|
+
}
|
|
24499
|
+
function yesNo(value, locale) {
|
|
24500
|
+
return locale === "zh-CN" ? formatChineseBoolean(value) : formatBoolean(value);
|
|
24501
|
+
}
|
|
24502
|
+
function localizeActionTitle(actionId, fallback, locale) {
|
|
24503
|
+
return locale === "zh-CN" ? translateReviewActionTitle(actionId, fallback) : fallback;
|
|
24504
|
+
}
|
|
24505
|
+
function localizeActionDetail(actionId, fallback, locale) {
|
|
24506
|
+
return locale === "zh-CN" ? translateReviewActionDetail(actionId, fallback) : fallback;
|
|
24507
|
+
}
|
|
24508
|
+
function localizeFocusReason(reasonId, fallback, locale) {
|
|
24509
|
+
return locale === "zh-CN" ? translateFocusReason(reasonId, fallback) : fallback;
|
|
24510
|
+
}
|
|
24511
|
+
function localizeDeduction(reasonId, fallback, locale) {
|
|
24512
|
+
return locale === "zh-CN" ? translateDeduction(reasonId, fallback) : fallback;
|
|
24513
|
+
}
|
|
24514
|
+
function escapeHtml(value) {
|
|
24515
|
+
return value
|
|
24516
|
+
.replace(/&/g, "&")
|
|
24517
|
+
.replace(/</g, "<")
|
|
24518
|
+
.replace(/>/g, ">")
|
|
24519
|
+
.replace(/"/g, """)
|
|
24520
|
+
.replace(/'/g, "'");
|
|
24521
|
+
}
|
|
23909
24522
|
function formatBoolean(value) {
|
|
23910
24523
|
return value ? "yes" : "no";
|
|
23911
24524
|
}
|
|
@@ -25133,7 +25746,7 @@ build_program
|
|
|
25133
25746
|
.option("--pr-body <body>", "Pull request body used for evidence checks.")
|
|
25134
25747
|
.option("--pr-body-file <path>", "Read a pull request body from a Markdown file.")
|
|
25135
25748
|
.option("--config <path>", "Path to .proofpr.yml.", ".proofpr.yml")
|
|
25136
|
-
.option("--format <format>", "Output format: markdown, json, or
|
|
25749
|
+
.option("--format <format>", "Output format: markdown, json, sarif, or html.", parseFormat, "markdown")
|
|
25137
25750
|
.option("--locale <locale>", "Report language: en or zh-CN.")
|
|
25138
25751
|
.option("--fail-on <level>", "Exit with code 1 on risk level: low, medium, high, or never.", parseFailLevel, "never")
|
|
25139
25752
|
.action(async (options) => {
|
|
@@ -25328,6 +25941,9 @@ function renderOutput(result, format, locale) {
|
|
|
25328
25941
|
if (format === "sarif") {
|
|
25329
25942
|
return renderSarifReport(result);
|
|
25330
25943
|
}
|
|
25944
|
+
if (format === "html") {
|
|
25945
|
+
return renderHtmlReport(result, locale);
|
|
25946
|
+
}
|
|
25331
25947
|
return renderMarkdownReport(result, locale);
|
|
25332
25948
|
}
|
|
25333
25949
|
async function runBenchmarks(casesDir) {
|
|
@@ -25481,10 +26097,10 @@ function matchesFindingExpectation(actualFindings, expected) {
|
|
|
25481
26097
|
return actualFindings.includes(expected);
|
|
25482
26098
|
}
|
|
25483
26099
|
function parseFormat(value) {
|
|
25484
|
-
if (value === "json" || value === "markdown" || value === "sarif") {
|
|
26100
|
+
if (value === "json" || value === "markdown" || value === "sarif" || value === "html") {
|
|
25485
26101
|
return value;
|
|
25486
26102
|
}
|
|
25487
|
-
throw new InvalidArgumentError("format must be one of: markdown, json, sarif");
|
|
26103
|
+
throw new InvalidArgumentError("format must be one of: markdown, json, sarif, html");
|
|
25488
26104
|
}
|
|
25489
26105
|
function parseBenchmarkFormat(value) {
|
|
25490
26106
|
if (value === "text" || value === "json" || value === "markdown") {
|