proof-pr 0.1.14 → 0.1.16
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 +14 -6
- package/dist/index.js +391 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ ProofPR 是给开源维护者和工程团队使用的 PR 证据门禁。它在
|
|
|
12
12
|
npx proof-pr@latest --version
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
当前应输出 `0.1.
|
|
15
|
+
当前应输出 `0.1.16`。
|
|
16
16
|
|
|
17
17
|
不知道用哪个功能时:
|
|
18
18
|
|
|
@@ -35,7 +35,13 @@ npx proof-pr@latest demo --list
|
|
|
35
35
|
npx proof-pr@latest init
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
这个命令会生成 `.proofpr.yml` 和 `.github/
|
|
38
|
+
这个命令会生成 `.proofpr.yml`、`.github/workflows/proofpr.yml` 和 `.github/pull_request_template.md`,提交后打开 PR 即可看到报告。
|
|
39
|
+
|
|
40
|
+
已接入仓库单独补 PR 模板:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx proof-pr@latest template
|
|
44
|
+
```
|
|
39
45
|
|
|
40
46
|
体检接入状态:
|
|
41
47
|
|
|
@@ -43,7 +49,7 @@ npx proof-pr@latest init
|
|
|
43
49
|
npx proof-pr@latest doctor
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
这个命令会检查配置文件、workflow、Action 版本、PR 权限和本地 diff 是否可读。
|
|
52
|
+
这个命令会检查配置文件、workflow、PR 模板、Action 版本、PR 权限和本地 diff 是否可读。
|
|
47
53
|
|
|
48
54
|
本地扫描当前分支:
|
|
49
55
|
|
|
@@ -54,7 +60,7 @@ npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN
|
|
|
54
60
|
扫描内置案例:
|
|
55
61
|
|
|
56
62
|
```bash
|
|
57
|
-
npx proof-pr@latest
|
|
63
|
+
npx proof-pr@latest demo workflow --locale zh-CN
|
|
58
64
|
```
|
|
59
65
|
|
|
60
66
|
生成独立 HTML 可视化报告:
|
|
@@ -63,6 +69,8 @@ npx proof-pr@latest scan --diff-file examples/cases/workflow-untrusted-checkout.
|
|
|
63
69
|
npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN --format html --output proofpr-report.html
|
|
64
70
|
```
|
|
65
71
|
|
|
72
|
+
HTML 报告支持筛选风险、搜索规则/文件/详情,并复制补证清单。
|
|
73
|
+
|
|
66
74
|
运行 benchmark:
|
|
67
75
|
|
|
68
76
|
```bash
|
|
@@ -72,7 +80,7 @@ npx proof-pr@latest benchmark --cases benchmarks/cases
|
|
|
72
80
|
## GitHub Action
|
|
73
81
|
|
|
74
82
|
```yaml
|
|
75
|
-
- uses: linsk27/proof-pr@v0.1.
|
|
83
|
+
- uses: linsk27/proof-pr@v0.1.16
|
|
76
84
|
with:
|
|
77
85
|
fail-on: high
|
|
78
86
|
comment: "true"
|
|
@@ -85,7 +93,7 @@ npx proof-pr@latest benchmark --cases benchmarks/cases
|
|
|
85
93
|
- 证据评分:0-100 分。
|
|
86
94
|
- Review 门禁:正常 review、重点 review、先补证据、风险处理前不要合并。
|
|
87
95
|
- Review 行动清单:维护者可直接执行的 checklist。
|
|
88
|
-
- 可选输出:GitHub annotations、SARIF、benchmark report
|
|
96
|
+
- 可选输出:GitHub annotations、SARIF、benchmark report、可筛选并可复制补证清单的独立 HTML 可视化报告;CLI 可用 `--output` 直接写文件。
|
|
89
97
|
|
|
90
98
|
## 常用预设
|
|
91
99
|
|
package/dist/index.js
CHANGED
|
@@ -23378,6 +23378,7 @@ function renderHtmlReport(result, locale = "en") {
|
|
|
23378
23378
|
const scoreGrade = formatEvidenceGrade(result.evidenceScore.grade, locale);
|
|
23379
23379
|
const findingsBySeverity = countFindingsBySeverity(result.findings);
|
|
23380
23380
|
const ruleCounts = countFindingsByRule(result.findings);
|
|
23381
|
+
const fixPrompt = renderContributorFixPrompt(result, locale);
|
|
23381
23382
|
const evidenceSignals = [
|
|
23382
23383
|
[labels.prDescription, locale === "zh-CN" ? translateDescriptionState(result.summary.pullRequestDescription) : result.summary.pullRequestDescription, result.summary.pullRequestDescription === "present"],
|
|
23383
23384
|
[labels.verification, yesNo(result.summary.verificationEvidence, locale), result.summary.verificationEvidence],
|
|
@@ -23549,6 +23550,69 @@ function renderHtmlReport(result, locale = "en") {
|
|
|
23549
23550
|
gap: 10px;
|
|
23550
23551
|
}
|
|
23551
23552
|
|
|
23553
|
+
.fix-panel {
|
|
23554
|
+
display: grid;
|
|
23555
|
+
gap: 12px;
|
|
23556
|
+
}
|
|
23557
|
+
|
|
23558
|
+
.fix-text {
|
|
23559
|
+
margin: 0;
|
|
23560
|
+
white-space: pre-wrap;
|
|
23561
|
+
border: 1px solid var(--line);
|
|
23562
|
+
border-radius: 8px;
|
|
23563
|
+
background: #fbfcfd;
|
|
23564
|
+
padding: 12px;
|
|
23565
|
+
color: var(--ink);
|
|
23566
|
+
overflow-x: auto;
|
|
23567
|
+
}
|
|
23568
|
+
|
|
23569
|
+
.copy-button, .filter-button {
|
|
23570
|
+
appearance: none;
|
|
23571
|
+
border: 1px solid var(--line);
|
|
23572
|
+
border-radius: 8px;
|
|
23573
|
+
background: var(--panel);
|
|
23574
|
+
color: var(--ink);
|
|
23575
|
+
font: inherit;
|
|
23576
|
+
font-size: 13px;
|
|
23577
|
+
padding: 8px 11px;
|
|
23578
|
+
cursor: pointer;
|
|
23579
|
+
}
|
|
23580
|
+
|
|
23581
|
+
.copy-button {
|
|
23582
|
+
justify-self: flex-start;
|
|
23583
|
+
border-color: #b8d5f4;
|
|
23584
|
+
background: var(--soft-blue);
|
|
23585
|
+
color: var(--blue);
|
|
23586
|
+
font-weight: 680;
|
|
23587
|
+
}
|
|
23588
|
+
|
|
23589
|
+
.filterbar {
|
|
23590
|
+
display: flex;
|
|
23591
|
+
flex-wrap: wrap;
|
|
23592
|
+
gap: 8px;
|
|
23593
|
+
margin-bottom: 12px;
|
|
23594
|
+
align-items: center;
|
|
23595
|
+
}
|
|
23596
|
+
|
|
23597
|
+
.filter-button.active {
|
|
23598
|
+
border-color: #b8d5f4;
|
|
23599
|
+
background: var(--soft-blue);
|
|
23600
|
+
color: var(--blue);
|
|
23601
|
+
font-weight: 680;
|
|
23602
|
+
}
|
|
23603
|
+
|
|
23604
|
+
.finding-search {
|
|
23605
|
+
min-width: min(320px, 100%);
|
|
23606
|
+
flex: 1 1 240px;
|
|
23607
|
+
border: 1px solid var(--line);
|
|
23608
|
+
border-radius: 8px;
|
|
23609
|
+
background: #fff;
|
|
23610
|
+
color: var(--ink);
|
|
23611
|
+
font: inherit;
|
|
23612
|
+
font-size: 13px;
|
|
23613
|
+
padding: 8px 11px;
|
|
23614
|
+
}
|
|
23615
|
+
|
|
23552
23616
|
.signal, .action, .focus, .deduction, .rule-row {
|
|
23553
23617
|
border: 1px solid var(--line);
|
|
23554
23618
|
border-radius: 8px;
|
|
@@ -23627,12 +23691,20 @@ function renderHtmlReport(result, locale = "en") {
|
|
|
23627
23691
|
background: #fff;
|
|
23628
23692
|
}
|
|
23629
23693
|
|
|
23694
|
+
.finding[hidden] { display: none; }
|
|
23695
|
+
|
|
23630
23696
|
.finding-head {
|
|
23631
23697
|
display: flex;
|
|
23632
23698
|
justify-content: space-between;
|
|
23633
23699
|
gap: 12px;
|
|
23634
23700
|
align-items: flex-start;
|
|
23635
23701
|
margin-bottom: 8px;
|
|
23702
|
+
cursor: pointer;
|
|
23703
|
+
list-style: none;
|
|
23704
|
+
}
|
|
23705
|
+
|
|
23706
|
+
.finding-head::-webkit-details-marker {
|
|
23707
|
+
display: none;
|
|
23636
23708
|
}
|
|
23637
23709
|
|
|
23638
23710
|
code {
|
|
@@ -23722,6 +23794,15 @@ function renderHtmlReport(result, locale = "en") {
|
|
|
23722
23794
|
</div>
|
|
23723
23795
|
</article>
|
|
23724
23796
|
|
|
23797
|
+
<article class="card full">
|
|
23798
|
+
<h2>${labels.quickFix}</h2>
|
|
23799
|
+
<div class="fix-panel">
|
|
23800
|
+
<p class="muted">${labels.quickFixHint}</p>
|
|
23801
|
+
<pre class="fix-text" id="proofpr-fix-prompt">${escapeHtml(fixPrompt)}</pre>
|
|
23802
|
+
<button class="copy-button" type="button" data-copy-target="proofpr-fix-prompt" data-label="${escapeHtml(labels.copyFix)}" data-copied="${escapeHtml(labels.copiedFix)}">${labels.copyFix}</button>
|
|
23803
|
+
</div>
|
|
23804
|
+
</article>
|
|
23805
|
+
|
|
23725
23806
|
<article class="card wide">
|
|
23726
23807
|
<h2>${labels.reviewPlan}</h2>
|
|
23727
23808
|
<div class="action-list">
|
|
@@ -23782,16 +23863,77 @@ function renderHtmlReport(result, locale = "en") {
|
|
|
23782
23863
|
|
|
23783
23864
|
<article class="card full">
|
|
23784
23865
|
<h2>${labels.findings}</h2>
|
|
23866
|
+
<div class="filterbar" aria-label="${labels.findingFilters}">
|
|
23867
|
+
${findingFilterButton(labels.allFindings, "all", result.findings.length, true)}
|
|
23868
|
+
${findingFilterButton(labels.high, "high", findingsBySeverity.high)}
|
|
23869
|
+
${findingFilterButton(labels.medium, "medium", findingsBySeverity.medium)}
|
|
23870
|
+
${findingFilterButton(labels.low, "low", findingsBySeverity.low)}
|
|
23871
|
+
${findingFilterButton(labels.info, "info", findingsBySeverity.info)}
|
|
23872
|
+
<input class="finding-search" id="proofpr-finding-search" type="search" placeholder="${escapeHtml(labels.searchFindings)}">
|
|
23873
|
+
</div>
|
|
23785
23874
|
<div class="finding-list">
|
|
23786
23875
|
${result.findings.length > 0
|
|
23787
23876
|
? result.findings.map((finding) => htmlFinding(finding, locale)).join("\n")
|
|
23788
23877
|
: `<div class="muted">${labels.noFindings}</div>`}
|
|
23878
|
+
<div class="muted" id="proofpr-empty-filter" hidden>${labels.noFilteredFindings}</div>
|
|
23789
23879
|
</div>
|
|
23790
23880
|
</article>
|
|
23791
23881
|
</section>
|
|
23792
23882
|
|
|
23793
23883
|
<p class="footer">${labels.footer}</p>
|
|
23794
23884
|
</main>
|
|
23885
|
+
<script>
|
|
23886
|
+
(() => {
|
|
23887
|
+
const buttons = Array.from(document.querySelectorAll("[data-filter-severity]"));
|
|
23888
|
+
const search = document.getElementById("proofpr-finding-search");
|
|
23889
|
+
const findings = Array.from(document.querySelectorAll("[data-finding]"));
|
|
23890
|
+
const empty = document.getElementById("proofpr-empty-filter");
|
|
23891
|
+
let activeSeverity = "all";
|
|
23892
|
+
|
|
23893
|
+
const applyFilters = () => {
|
|
23894
|
+
const query = (search?.value || "").trim().toLowerCase();
|
|
23895
|
+
let visible = 0;
|
|
23896
|
+
|
|
23897
|
+
for (const finding of findings) {
|
|
23898
|
+
const severity = finding.getAttribute("data-severity") || "";
|
|
23899
|
+
const haystack = finding.getAttribute("data-search") || "";
|
|
23900
|
+
const severityMatches = activeSeverity === "all" || severity === activeSeverity;
|
|
23901
|
+
const queryMatches = query === "" || haystack.includes(query);
|
|
23902
|
+
const show = severityMatches && queryMatches;
|
|
23903
|
+
finding.hidden = !show;
|
|
23904
|
+
if (show) visible += 1;
|
|
23905
|
+
}
|
|
23906
|
+
|
|
23907
|
+
if (empty) empty.hidden = visible !== 0 || findings.length === 0;
|
|
23908
|
+
};
|
|
23909
|
+
|
|
23910
|
+
for (const button of buttons) {
|
|
23911
|
+
button.addEventListener("click", () => {
|
|
23912
|
+
activeSeverity = button.getAttribute("data-filter-severity") || "all";
|
|
23913
|
+
for (const item of buttons) item.classList.toggle("active", item === button);
|
|
23914
|
+
applyFilters();
|
|
23915
|
+
});
|
|
23916
|
+
}
|
|
23917
|
+
|
|
23918
|
+
search?.addEventListener("input", applyFilters);
|
|
23919
|
+
|
|
23920
|
+
for (const button of Array.from(document.querySelectorAll("[data-copy-target]"))) {
|
|
23921
|
+
button.addEventListener("click", async () => {
|
|
23922
|
+
const target = document.getElementById(button.getAttribute("data-copy-target") || "");
|
|
23923
|
+
const text = target?.textContent || "";
|
|
23924
|
+
try {
|
|
23925
|
+
await navigator.clipboard.writeText(text);
|
|
23926
|
+
button.textContent = button.getAttribute("data-copied") || button.textContent;
|
|
23927
|
+
setTimeout(() => {
|
|
23928
|
+
button.textContent = button.getAttribute("data-label") || button.textContent;
|
|
23929
|
+
}, 1200);
|
|
23930
|
+
} catch {
|
|
23931
|
+
button.textContent = text;
|
|
23932
|
+
}
|
|
23933
|
+
});
|
|
23934
|
+
}
|
|
23935
|
+
})();
|
|
23936
|
+
</script>
|
|
23795
23937
|
</body>
|
|
23796
23938
|
</html>
|
|
23797
23939
|
`;
|
|
@@ -23844,9 +23986,14 @@ function renderEnglishMarkdownReport(result) {
|
|
|
23844
23986
|
REPORT_MARKER,
|
|
23845
23987
|
"# ProofPR Review",
|
|
23846
23988
|
"",
|
|
23847
|
-
|
|
23848
|
-
|
|
23849
|
-
|
|
23989
|
+
"## Summary",
|
|
23990
|
+
"",
|
|
23991
|
+
"| Item | Result |",
|
|
23992
|
+
"| --- | --- |",
|
|
23993
|
+
`| Risk | **${result.risk}** |`,
|
|
23994
|
+
`| Evidence score | **${result.evidenceScore.value}/100 (${formatEvidenceGrade(result.evidenceScore.grade, "en")})** |`,
|
|
23995
|
+
`| Review gate | **${formatReviewDecision(result.reviewDecision, "en")}** |`,
|
|
23996
|
+
`| Findings | ${result.findings.length} |`,
|
|
23850
23997
|
"",
|
|
23851
23998
|
"## Evidence",
|
|
23852
23999
|
"",
|
|
@@ -23864,6 +24011,7 @@ function renderEnglishMarkdownReport(result) {
|
|
|
23864
24011
|
""
|
|
23865
24012
|
];
|
|
23866
24013
|
appendEvidenceScoreSection(lines, result, "en");
|
|
24014
|
+
appendQuickFixSection(lines, result, "en");
|
|
23867
24015
|
appendReviewPlanSection(lines, result, "en");
|
|
23868
24016
|
if (result.findings.length === 0) {
|
|
23869
24017
|
lines.push("## Findings", "", "No review-risk findings detected by the enabled rules.", "");
|
|
@@ -23881,9 +24029,14 @@ function renderChineseMarkdownReport(result) {
|
|
|
23881
24029
|
REPORT_MARKER,
|
|
23882
24030
|
"# ProofPR 审查报告",
|
|
23883
24031
|
"",
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
|
|
24032
|
+
"## 总览",
|
|
24033
|
+
"",
|
|
24034
|
+
"| 项目 | 结果 |",
|
|
24035
|
+
"| --- | --- |",
|
|
24036
|
+
`| 风险等级 | **${translateRisk(result.risk)}** |`,
|
|
24037
|
+
`| 证据评分 | **${result.evidenceScore.value}/100(${formatEvidenceGrade(result.evidenceScore.grade, "zh-CN")})** |`,
|
|
24038
|
+
`| Review 门禁 | **${formatReviewDecision(result.reviewDecision, "zh-CN")}** |`,
|
|
24039
|
+
`| 风险发现 | ${result.findings.length} |`,
|
|
23887
24040
|
"",
|
|
23888
24041
|
"## 证据概览",
|
|
23889
24042
|
"",
|
|
@@ -23901,6 +24054,7 @@ function renderChineseMarkdownReport(result) {
|
|
|
23901
24054
|
""
|
|
23902
24055
|
];
|
|
23903
24056
|
appendEvidenceScoreSection(lines, result, "zh-CN");
|
|
24057
|
+
appendQuickFixSection(lines, result, "zh-CN");
|
|
23904
24058
|
appendReviewPlanSection(lines, result, "zh-CN");
|
|
23905
24059
|
if (result.findings.length === 0) {
|
|
23906
24060
|
lines.push("## 风险发现", "", "启用的规则没有发现需要优先关注的 review 风险。", "");
|
|
@@ -23937,6 +24091,13 @@ function appendEvidenceScoreSection(lines, result, locale) {
|
|
|
23937
24091
|
}
|
|
23938
24092
|
lines.push("");
|
|
23939
24093
|
}
|
|
24094
|
+
function appendQuickFixSection(lines, result, locale) {
|
|
24095
|
+
lines.push(locale === "zh-CN" ? "## 可复制补证清单" : "## Copyable Fix Checklist", "");
|
|
24096
|
+
lines.push(locale === "zh-CN"
|
|
24097
|
+
? "贡献者可以直接复制下面内容补到 PR 描述里,维护者也可以把它作为 review 回复。"
|
|
24098
|
+
: "Contributors can paste this into the PR description; maintainers can also use it as a review reply.");
|
|
24099
|
+
lines.push("", "```md", renderContributorFixPrompt(result, locale), "```", "");
|
|
24100
|
+
}
|
|
23940
24101
|
function appendReviewPlanSection(lines, result, locale) {
|
|
23941
24102
|
lines.push(locale === "zh-CN" ? "## Review 行动清单" : "## Review Plan", "");
|
|
23942
24103
|
if (result.reviewPlan.actionItems.length > 0) {
|
|
@@ -23959,6 +24120,97 @@ function appendReviewPlanSection(lines, result, locale) {
|
|
|
23959
24120
|
}
|
|
23960
24121
|
lines.push("");
|
|
23961
24122
|
}
|
|
24123
|
+
function renderContributorFixPrompt(result, locale) {
|
|
24124
|
+
const missingEvidence = missingEvidenceLabels(result, locale);
|
|
24125
|
+
const actions = result.reviewPlan.actionItems.slice(0, 6);
|
|
24126
|
+
const focusFiles = result.reviewPlan.focusFiles.slice(0, 5);
|
|
24127
|
+
if (locale === "zh-CN") {
|
|
24128
|
+
const lines = [
|
|
24129
|
+
"请在这个 PR 描述中补充以下内容,方便维护者继续 review:",
|
|
24130
|
+
missingEvidence.length > 0 ? `ProofPR 当前最缺:${missingEvidence.join("、")}。` : "ProofPR 当前没有发现必须补充的证据项,可以保留关键验证记录。",
|
|
24131
|
+
"",
|
|
24132
|
+
"## 验证方式",
|
|
24133
|
+
"- 自动化测试:",
|
|
24134
|
+
"- 手动验证:",
|
|
24135
|
+
"- 未覆盖或不适用的部分:",
|
|
24136
|
+
"",
|
|
24137
|
+
"## 复现 / Before & After",
|
|
24138
|
+
"- 复现步骤或改动前状态:",
|
|
24139
|
+
"- 改动后结果:",
|
|
24140
|
+
"- 截图 / 录屏 / 日志链接:",
|
|
24141
|
+
"",
|
|
24142
|
+
"## 风险说明",
|
|
24143
|
+
"- 依赖 / CI / 权限 / MCP 变更原因:",
|
|
24144
|
+
"- 发布影响、迁移说明或回滚方案:"
|
|
24145
|
+
];
|
|
24146
|
+
if (actions.length > 0) {
|
|
24147
|
+
lines.push("", "## ProofPR 需要处理的点");
|
|
24148
|
+
for (const action of actions) {
|
|
24149
|
+
lines.push(`- ${translateReviewActionTitle(action.actionId, action.title)}:${translateReviewActionDetail(action.actionId, action.detail)}`);
|
|
24150
|
+
}
|
|
24151
|
+
}
|
|
24152
|
+
if (focusFiles.length > 0) {
|
|
24153
|
+
lines.push("", "## 重点文件");
|
|
24154
|
+
for (const file of focusFiles) {
|
|
24155
|
+
lines.push(`- ${file.path}:${translateFocusReason(file.reasonId, file.reason)}`);
|
|
24156
|
+
}
|
|
24157
|
+
}
|
|
24158
|
+
return lines.join("\n");
|
|
24159
|
+
}
|
|
24160
|
+
const lines = [
|
|
24161
|
+
"Please add the following context to this PR so maintainers can continue review:",
|
|
24162
|
+
missingEvidence.length > 0 ? `ProofPR is currently missing: ${missingEvidence.join(", ")}.` : "ProofPR did not find required missing evidence; keep the key verification notes visible.",
|
|
24163
|
+
"",
|
|
24164
|
+
"## Verification",
|
|
24165
|
+
"- Automated tests:",
|
|
24166
|
+
"- Manual verification:",
|
|
24167
|
+
"- Not covered or not applicable:",
|
|
24168
|
+
"",
|
|
24169
|
+
"## Reproduction / Before & After",
|
|
24170
|
+
"- Reproduction steps or previous state:",
|
|
24171
|
+
"- Result after this change:",
|
|
24172
|
+
"- Screenshot / recording / log link:",
|
|
24173
|
+
"",
|
|
24174
|
+
"## Risk Notes",
|
|
24175
|
+
"- Dependency / CI / permission / MCP rationale:",
|
|
24176
|
+
"- Release impact, migration notes, or rollback plan:"
|
|
24177
|
+
];
|
|
24178
|
+
if (actions.length > 0) {
|
|
24179
|
+
lines.push("", "## ProofPR Items To Resolve");
|
|
24180
|
+
for (const action of actions) {
|
|
24181
|
+
lines.push(`- ${action.title}: ${action.detail}`);
|
|
24182
|
+
}
|
|
24183
|
+
}
|
|
24184
|
+
if (focusFiles.length > 0) {
|
|
24185
|
+
lines.push("", "## Focus Files");
|
|
24186
|
+
for (const file of focusFiles) {
|
|
24187
|
+
lines.push(`- ${file.path}: ${file.reason}`);
|
|
24188
|
+
}
|
|
24189
|
+
}
|
|
24190
|
+
return lines.join("\n");
|
|
24191
|
+
}
|
|
24192
|
+
function missingEvidenceLabels(result, locale) {
|
|
24193
|
+
const labels = [];
|
|
24194
|
+
if (result.summary.pullRequestDescription !== "present") {
|
|
24195
|
+
labels.push(locale === "zh-CN" ? "清楚的 PR 描述" : "clear PR description");
|
|
24196
|
+
}
|
|
24197
|
+
if (!result.summary.verificationEvidence) {
|
|
24198
|
+
labels.push(locale === "zh-CN" ? "测试或手动验证" : "test or manual verification");
|
|
24199
|
+
}
|
|
24200
|
+
if (!result.summary.reproductionEvidence) {
|
|
24201
|
+
labels.push(locale === "zh-CN" ? "复现步骤或 before/after" : "reproduction steps or before/after context");
|
|
24202
|
+
}
|
|
24203
|
+
if (!result.summary.screenshotEvidence && result.findings.some((finding) => finding.ruleId.includes("screenshot"))) {
|
|
24204
|
+
labels.push(locale === "zh-CN" ? "截图或录屏" : "screenshot or recording");
|
|
24205
|
+
}
|
|
24206
|
+
if (!result.summary.changelogEvidence && result.findings.some((finding) => finding.ruleId.includes("dependency-major-upgrade"))) {
|
|
24207
|
+
labels.push(locale === "zh-CN" ? "changelog 或迁移说明" : "changelog or migration notes");
|
|
24208
|
+
}
|
|
24209
|
+
if (!result.summary.permissionRationaleEvidence && result.findings.some((finding) => finding.ruleId.includes("workflow"))) {
|
|
24210
|
+
labels.push(locale === "zh-CN" ? "权限变更理由" : "permission rationale");
|
|
24211
|
+
}
|
|
24212
|
+
return labels;
|
|
24213
|
+
}
|
|
23962
24214
|
function formatEnglishFinding(finding) {
|
|
23963
24215
|
const lines = [
|
|
23964
24216
|
`### ${finding.title}`,
|
|
@@ -24360,7 +24612,15 @@ function htmlLabels(locale) {
|
|
|
24360
24612
|
permissionRationale: "权限理由",
|
|
24361
24613
|
reviewPlan: "Review 行动清单",
|
|
24362
24614
|
noActions: "没有额外行动项。",
|
|
24615
|
+
quickFix: "一键补证建议",
|
|
24616
|
+
quickFixHint: "复制这段内容到 PR 描述或评论里,贡献者按空白项补齐即可。",
|
|
24617
|
+
copyFix: "复制补证清单",
|
|
24618
|
+
copiedFix: "已复制",
|
|
24363
24619
|
findingDistribution: "Finding 分布",
|
|
24620
|
+
findingFilters: "筛选风险发现",
|
|
24621
|
+
allFindings: "全部",
|
|
24622
|
+
searchFindings: "搜索规则、文件或详情",
|
|
24623
|
+
noFilteredFindings: "当前筛选条件下没有风险发现。",
|
|
24364
24624
|
high: "高",
|
|
24365
24625
|
medium: "中",
|
|
24366
24626
|
low: "低",
|
|
@@ -24408,7 +24668,15 @@ function htmlLabels(locale) {
|
|
|
24408
24668
|
permissionRationale: "Permission rationale",
|
|
24409
24669
|
reviewPlan: "Review plan",
|
|
24410
24670
|
noActions: "No additional action items.",
|
|
24671
|
+
quickFix: "One-click evidence fix",
|
|
24672
|
+
quickFixHint: "Copy this into the PR description or a review reply, then fill the blanks.",
|
|
24673
|
+
copyFix: "Copy checklist",
|
|
24674
|
+
copiedFix: "Copied",
|
|
24411
24675
|
findingDistribution: "Finding distribution",
|
|
24676
|
+
findingFilters: "Filter findings",
|
|
24677
|
+
allFindings: "All",
|
|
24678
|
+
searchFindings: "Search rules, files, or details",
|
|
24679
|
+
noFilteredFindings: "No findings match the current filter.",
|
|
24412
24680
|
high: "High",
|
|
24413
24681
|
medium: "Medium",
|
|
24414
24682
|
low: "Low",
|
|
@@ -24438,9 +24706,25 @@ function signalItem(name, state, ok) {
|
|
|
24438
24706
|
function severityItem(severity, value, label) {
|
|
24439
24707
|
return `<div class="severity ${severity === "high" ? "tone-high" : severity === "medium" ? "tone-medium" : severity === "low" ? "tone-low" : ""}"><strong>${value}</strong><span>${escapeHtml(label)}</span></div>`;
|
|
24440
24708
|
}
|
|
24709
|
+
function findingFilterButton(label, severity, count, active = false) {
|
|
24710
|
+
return `<button class="filter-button${active ? " active" : ""}" type="button" data-filter-severity="${escapeHtml(severity)}">${escapeHtml(label)} <span class="muted">${count}</span></button>`;
|
|
24711
|
+
}
|
|
24441
24712
|
function htmlFinding(finding, locale) {
|
|
24442
24713
|
const labels = htmlLabels(locale);
|
|
24443
24714
|
const translated = locale === "zh-CN" ? translateFinding(finding) : finding;
|
|
24715
|
+
const searchText = [
|
|
24716
|
+
finding.ruleId,
|
|
24717
|
+
finding.severity,
|
|
24718
|
+
finding.path,
|
|
24719
|
+
translated.title,
|
|
24720
|
+
translated.message,
|
|
24721
|
+
translated.recommendation,
|
|
24722
|
+
...(finding.evidence ?? [])
|
|
24723
|
+
]
|
|
24724
|
+
.filter(Boolean)
|
|
24725
|
+
.join(" ")
|
|
24726
|
+
.toLowerCase()
|
|
24727
|
+
.replace(/\s+/g, " ");
|
|
24444
24728
|
const evidence = finding.evidence && finding.evidence.length > 0
|
|
24445
24729
|
? `<ul class="evidence-list">${finding.evidence
|
|
24446
24730
|
.map((item) => `<li><code>${escapeHtml(locale === "zh-CN" ? translateEvidence(item) : item)}</code></li>`)
|
|
@@ -24452,19 +24736,19 @@ function htmlFinding(finding, locale) {
|
|
|
24452
24736
|
const recommendation = translated.recommendation
|
|
24453
24737
|
? `<div class="muted">${labels.recommendation}: ${escapeHtml(translated.recommendation)}</div>`
|
|
24454
24738
|
: "";
|
|
24455
|
-
return `<
|
|
24456
|
-
<
|
|
24739
|
+
return `<details class="finding" open data-finding data-severity="${escapeHtml(finding.severity)}" data-search="${escapeHtml(searchText)}">
|
|
24740
|
+
<summary class="finding-head">
|
|
24457
24741
|
<div>
|
|
24458
24742
|
<div class="finding-title">${escapeHtml(translated.title)}</div>
|
|
24459
24743
|
<div class="muted">${labels.rule}: <code>${escapeHtml(finding.ruleId)}</code></div>
|
|
24460
24744
|
</div>
|
|
24461
24745
|
<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>
|
|
24462
|
-
</
|
|
24746
|
+
</summary>
|
|
24463
24747
|
${path}
|
|
24464
24748
|
<div class="muted">${labels.detail}: ${escapeHtml(translated.message)}</div>
|
|
24465
24749
|
${evidence}
|
|
24466
24750
|
${recommendation}
|
|
24467
|
-
</
|
|
24751
|
+
</details>`;
|
|
24468
24752
|
}
|
|
24469
24753
|
function countFindingsBySeverity(findings) {
|
|
24470
24754
|
return findings.reduce((counts, finding) => {
|
|
@@ -25728,7 +26012,7 @@ function dedupeFindings(findings) {
|
|
|
25728
26012
|
|
|
25729
26013
|
|
|
25730
26014
|
const execFileAsync = (0,external_node_util_namespaceObject.promisify)(external_node_child_process_.execFile);
|
|
25731
|
-
const CLI_VERSION = "0.1.
|
|
26015
|
+
const CLI_VERSION = "0.1.16";
|
|
25732
26016
|
const DEMO_CASES = [
|
|
25733
26017
|
{
|
|
25734
26018
|
id: "workflow",
|
|
@@ -25869,6 +26153,7 @@ build_program
|
|
|
25869
26153
|
.description("Check whether ProofPR is installed correctly in the current repository.")
|
|
25870
26154
|
.option("--config <path>", "Path to .proofpr.yml.", ".proofpr.yml")
|
|
25871
26155
|
.option("--workflow-path <path>", "Path to the GitHub Actions workflow.", ".github/workflows/proofpr.yml")
|
|
26156
|
+
.option("--pr-template-path <path>", "Path to the pull request template.", ".github/pull_request_template.md")
|
|
25872
26157
|
.option("--base <ref>", "Base git ref used for local diff checks.", "origin/main")
|
|
25873
26158
|
.option("--head <ref>", "Head git ref used for local diff checks.", "HEAD")
|
|
25874
26159
|
.action(async (options) => {
|
|
@@ -25878,6 +26163,15 @@ build_program
|
|
|
25878
26163
|
process.exitCode = 1;
|
|
25879
26164
|
}
|
|
25880
26165
|
});
|
|
26166
|
+
build_program
|
|
26167
|
+
.command("template")
|
|
26168
|
+
.description("Create a ProofPR-friendly pull request template.")
|
|
26169
|
+
.option("--output <path>", "Path to write the pull request template.", ".github/pull_request_template.md")
|
|
26170
|
+
.option("--force", "Overwrite the existing template.", false)
|
|
26171
|
+
.action(async (options) => {
|
|
26172
|
+
await writeIfMissing(options.output, renderPullRequestTemplate(), options.force);
|
|
26173
|
+
process.stdout.write(`ProofPR pull request template written to ${options.output}\n\nNext:\n1. Commit the template.\n2. Ask contributors to fill verification, reproduction, screenshot, changelog, and permission rationale sections when relevant.\n3. Run npx proof-pr@latest doctor to check setup.\n`);
|
|
26174
|
+
});
|
|
25881
26175
|
build_program
|
|
25882
26176
|
.command("demo")
|
|
25883
26177
|
.description("Run a built-in ProofPR demo case without cloning this repository.")
|
|
@@ -25951,13 +26245,26 @@ build_program
|
|
|
25951
26245
|
.description("Create a starter .proofpr.yml and GitHub Actions workflow.")
|
|
25952
26246
|
.option("--config-path <path>", "Path to write the ProofPR configuration file.", ".proofpr.yml")
|
|
25953
26247
|
.option("--workflow-path <path>", "Path to write the GitHub Actions workflow.", ".github/workflows/proofpr.yml")
|
|
26248
|
+
.option("--no-pr-template", "Skip creating .github/pull_request_template.md.")
|
|
26249
|
+
.option("--pr-template-path <path>", "Path to write the pull request template.", ".github/pull_request_template.md")
|
|
25954
26250
|
.option("--preset <preset>", `Config preset: ${listConfigPresets().join(", ")}.`, parsePresetOption, "open-source-maintainer")
|
|
25955
26251
|
.option("--fail-on <level>", "Workflow failure threshold: low, medium, high, or never.", parseFailLevel, "high")
|
|
25956
26252
|
.option("--force", "Overwrite existing files.", false)
|
|
25957
26253
|
.action(async (options) => {
|
|
25958
26254
|
await writeIfMissing(options.configPath, renderConfigTemplate(options.preset), options.force);
|
|
25959
26255
|
await writeIfMissing(options.workflowPath, renderWorkflowTemplate(options.failOn), options.force);
|
|
25960
|
-
|
|
26256
|
+
const created = [options.configPath, options.workflowPath];
|
|
26257
|
+
const skipped = [];
|
|
26258
|
+
if (options.prTemplate) {
|
|
26259
|
+
const wroteTemplate = await writeIfMissingSoft(options.prTemplatePath, renderPullRequestTemplate(), options.force);
|
|
26260
|
+
if (wroteTemplate) {
|
|
26261
|
+
created.push(options.prTemplatePath);
|
|
26262
|
+
}
|
|
26263
|
+
else {
|
|
26264
|
+
skipped.push(`${options.prTemplatePath} already exists`);
|
|
26265
|
+
}
|
|
26266
|
+
}
|
|
26267
|
+
process.stdout.write(`ProofPR initialized.\n\nCreated:\n${created.map((item) => `- ${item}`).join("\n")}${skipped.length > 0 ? `\n\nSkipped:\n${skipped.map((item) => `- ${item}`).join("\n")}` : ""}\n\nNext:\n1. Commit these files.\n2. Open or update a pull request.\n3. Read the ProofPR comment or Actions summary.\n\nLocal check:\nnpx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN\n\nNeed another task?\nnpx proof-pr@latest guide\n`);
|
|
25961
26268
|
});
|
|
25962
26269
|
build_program
|
|
25963
26270
|
.command("benchmark")
|
|
@@ -26004,36 +26311,40 @@ function renderGuide() {
|
|
|
26004
26311
|
|
|
26005
26312
|
1. 接入 GitHub PR 自动检查
|
|
26006
26313
|
npx proof-pr@latest init
|
|
26007
|
-
然后提交 .proofpr.yml 和 .github/
|
|
26314
|
+
然后提交 .proofpr.yml、.github/workflows/proofpr.yml 和 .github/pull_request_template.md,打开 PR 后看评论和 Actions summary。
|
|
26008
26315
|
|
|
26009
26316
|
2. 体检当前仓库接入状态
|
|
26010
26317
|
npx proof-pr@latest doctor
|
|
26011
|
-
检查配置文件、workflow、Action 版本、PR 权限和本地 diff 是否正常。
|
|
26318
|
+
检查配置文件、workflow、PR 模板、Action 版本、PR 权限和本地 diff 是否正常。
|
|
26319
|
+
|
|
26320
|
+
3. 已接入仓库,单独补 PR 模板
|
|
26321
|
+
npx proof-pr@latest template
|
|
26322
|
+
引导贡献者填写验证、复现、截图、changelog 和权限理由。
|
|
26012
26323
|
|
|
26013
|
-
|
|
26324
|
+
4. 本地检查当前分支
|
|
26014
26325
|
npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN
|
|
26015
26326
|
适合在发 PR 前先看风险、证据评分和 Review 行动清单。
|
|
26016
26327
|
|
|
26017
|
-
|
|
26328
|
+
5. 生成可分享 HTML 报告
|
|
26018
26329
|
npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN --format html --output proofpr-report.html
|
|
26019
26330
|
生成后用浏览器打开 proofpr-report.html。
|
|
26020
26331
|
|
|
26021
|
-
|
|
26332
|
+
6. 生成 GitHub Code Scanning 的 SARIF
|
|
26022
26333
|
npx proof-pr@latest scan --base origin/main --head HEAD --format sarif --output proofpr.sarif
|
|
26023
26334
|
适合在 CI 里配合 github/codeql-action/upload-sarif 使用。
|
|
26024
26335
|
|
|
26025
|
-
|
|
26336
|
+
7. 不接入仓库,先试跑内置案例
|
|
26026
26337
|
npx proof-pr@latest demo workflow --locale zh-CN
|
|
26027
26338
|
不需要 clone 仓库或寻找 examples 文件,也能快速看到 ProofPR 会抓什么风险。
|
|
26028
26339
|
|
|
26029
|
-
|
|
26340
|
+
8. 查看所有内置案例
|
|
26030
26341
|
npx proof-pr@latest demo --list
|
|
26031
26342
|
|
|
26032
|
-
|
|
26343
|
+
9. 验证规则样本是否仍然命中
|
|
26033
26344
|
npx proof-pr@latest benchmark --cases benchmarks/cases
|
|
26034
26345
|
适合维护 ProofPR 规则或发版前回归。
|
|
26035
26346
|
|
|
26036
|
-
|
|
26347
|
+
10. 调整审查强度
|
|
26037
26348
|
打开 .proofpr.yml,把 preset 改成 security-strict、dependency-careful 或 mcp-security。
|
|
26038
26349
|
|
|
26039
26350
|
结果在哪里看:
|
|
@@ -26113,6 +26424,7 @@ async function runDoctor(options) {
|
|
|
26113
26424
|
checks.push({ level: "fail", title: `缺少 ${options.workflowPath}` });
|
|
26114
26425
|
nextSteps.add("运行 npx proof-pr@latest init 生成 .github/workflows/proofpr.yml。");
|
|
26115
26426
|
}
|
|
26427
|
+
await inspectPullRequestTemplate(options.prTemplatePath, checks, nextSteps);
|
|
26116
26428
|
await inspectGitDiff(options, checks, nextSteps);
|
|
26117
26429
|
if (nextSteps.size === 0) {
|
|
26118
26430
|
nextSteps.add("当前接入状态正常;可以打开 PR,或运行 npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN 做本地自查。");
|
|
@@ -26158,6 +26470,29 @@ function inspectWorkflow(workflow, checks, nextSteps) {
|
|
|
26158
26470
|
nextSteps.add("建议在 workflow permissions 中加入 contents: read。");
|
|
26159
26471
|
}
|
|
26160
26472
|
}
|
|
26473
|
+
async function inspectPullRequestTemplate(path, checks, nextSteps) {
|
|
26474
|
+
if (!(await pathExists(path))) {
|
|
26475
|
+
checks.push({ level: "warn", title: `缺少 ${path}` });
|
|
26476
|
+
nextSteps.add("运行 npx proof-pr@latest template 生成 PR 模板,引导贡献者补充验证、复现、截图和权限理由。");
|
|
26477
|
+
return;
|
|
26478
|
+
}
|
|
26479
|
+
const template = await (0,promises_namespaceObject.readFile)(path, "utf8");
|
|
26480
|
+
checks.push({ level: "pass", title: `${path} 已存在` });
|
|
26481
|
+
if (/验证|verification|test/i.test(template)) {
|
|
26482
|
+
checks.push({ level: "pass", title: "PR 模板会提示验证证据" });
|
|
26483
|
+
}
|
|
26484
|
+
else {
|
|
26485
|
+
checks.push({ level: "warn", title: "PR 模板没有明显的验证证据提示" });
|
|
26486
|
+
nextSteps.add("在 PR 模板中加入“验证方式”栏目,减少 ProofPR 报告里的证据不足。");
|
|
26487
|
+
}
|
|
26488
|
+
if (/复现|reproduction|before|after|截图|screenshot|权限|permission/i.test(template)) {
|
|
26489
|
+
checks.push({ level: "pass", title: "PR 模板覆盖复现、截图或权限理由提示" });
|
|
26490
|
+
}
|
|
26491
|
+
else {
|
|
26492
|
+
checks.push({ level: "warn", title: "PR 模板缺少复现、截图或权限理由提示" });
|
|
26493
|
+
nextSteps.add("在 PR 模板中加入复现、截图、changelog、权限理由等可选栏目。");
|
|
26494
|
+
}
|
|
26495
|
+
}
|
|
26161
26496
|
async function inspectGitDiff(options, checks, nextSteps) {
|
|
26162
26497
|
try {
|
|
26163
26498
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -26244,6 +26579,14 @@ async function writeIfMissing(path, contents, force) {
|
|
|
26244
26579
|
await (0,promises_namespaceObject.mkdir)((0,external_node_path_.dirname)(path), { recursive: true });
|
|
26245
26580
|
await (0,promises_namespaceObject.writeFile)(path, contents, "utf8");
|
|
26246
26581
|
}
|
|
26582
|
+
async function writeIfMissingSoft(path, contents, force) {
|
|
26583
|
+
if (!force && (await pathExists(path))) {
|
|
26584
|
+
return false;
|
|
26585
|
+
}
|
|
26586
|
+
await (0,promises_namespaceObject.mkdir)((0,external_node_path_.dirname)(path), { recursive: true });
|
|
26587
|
+
await (0,promises_namespaceObject.writeFile)(path, contents, "utf8");
|
|
26588
|
+
return true;
|
|
26589
|
+
}
|
|
26247
26590
|
async function writeOutput(path, contents) {
|
|
26248
26591
|
await (0,promises_namespaceObject.mkdir)((0,external_node_path_.dirname)(path), { recursive: true });
|
|
26249
26592
|
await (0,promises_namespaceObject.writeFile)(path, contents, "utf8");
|
|
@@ -26291,6 +26634,33 @@ jobs:
|
|
|
26291
26634
|
annotations: "true"
|
|
26292
26635
|
`;
|
|
26293
26636
|
}
|
|
26637
|
+
function renderPullRequestTemplate() {
|
|
26638
|
+
return `## 变更说明
|
|
26639
|
+
|
|
26640
|
+
请说明这个 PR 为什么需要、改了什么、影响范围是什么。
|
|
26641
|
+
|
|
26642
|
+
## 验证方式
|
|
26643
|
+
|
|
26644
|
+
- [ ] 已运行自动化测试:
|
|
26645
|
+
- [ ] 已完成手动验证:
|
|
26646
|
+
- [ ] 不需要测试,原因:
|
|
26647
|
+
|
|
26648
|
+
## 复现 / Before & After
|
|
26649
|
+
|
|
26650
|
+
如果是 bug fix,请写复现步骤、预期结果和实际结果。
|
|
26651
|
+
如果是 UI 改动,请附 before/after 截图或录屏。
|
|
26652
|
+
|
|
26653
|
+
## 依赖 / CI / 权限 / MCP 变更
|
|
26654
|
+
|
|
26655
|
+
如果改了依赖、lockfile、GitHub Actions、MCP、环境变量或权限,请说明原因和安全影响。
|
|
26656
|
+
|
|
26657
|
+
## 发布风险
|
|
26658
|
+
|
|
26659
|
+
- [ ] 无破坏性变更
|
|
26660
|
+
- [ ] 需要迁移说明 / changelog
|
|
26661
|
+
- [ ] 需要灰度或回滚方案
|
|
26662
|
+
`;
|
|
26663
|
+
}
|
|
26294
26664
|
function renderOutput(result, format, locale) {
|
|
26295
26665
|
if (format === "json") {
|
|
26296
26666
|
return JSON.stringify(result, null, 2);
|