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.
Files changed (3) hide show
  1. package/README.md +14 -6
  2. package/dist/index.js +391 -21
  3. 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.14`。
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/workflows/proofpr.yml`,提交后打开 PR 即可看到报告。
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 scan --diff-file examples/cases/workflow-untrusted-checkout.diff --locale zh-CN
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.14
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、独立 HTML 可视化报告;CLI 可用 `--output` 直接写文件。
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
- `Risk: **${result.risk}**`,
23848
- `Evidence score: **${result.evidenceScore.value}/100 (${formatEvidenceGrade(result.evidenceScore.grade, "en")})**`,
23849
- `Review gate: **${formatReviewDecision(result.reviewDecision, "en")}**`,
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
- `风险等级:**${translateRisk(result.risk)}**`,
23885
- `证据评分:**${result.evidenceScore.value}/100(${formatEvidenceGrade(result.evidenceScore.grade, "zh-CN")})**`,
23886
- `Review 门禁:**${formatReviewDecision(result.reviewDecision, "zh-CN")}**`,
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 `<div class="finding">
24456
- <div class="finding-head">
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
- </div>
24746
+ </summary>
24463
24747
  ${path}
24464
24748
  <div class="muted">${labels.detail}: ${escapeHtml(translated.message)}</div>
24465
24749
  ${evidence}
24466
24750
  ${recommendation}
24467
- </div>`;
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.14";
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
- process.stdout.write(`ProofPR initialized.\n\nCreated:\n- ${options.configPath}\n- ${options.workflowPath}\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`);
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/workflows/proofpr.yml,打开 PR 后看评论和 Actions summary。
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
- 3. 本地检查当前分支
26324
+ 4. 本地检查当前分支
26014
26325
  npx proof-pr@latest scan --base origin/main --head HEAD --locale zh-CN
26015
26326
  适合在发 PR 前先看风险、证据评分和 Review 行动清单。
26016
26327
 
26017
- 4. 生成可分享 HTML 报告
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
- 5. 生成 GitHub Code Scanning 的 SARIF
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
- 6. 不接入仓库,先试跑内置案例
26336
+ 7. 不接入仓库,先试跑内置案例
26026
26337
  npx proof-pr@latest demo workflow --locale zh-CN
26027
26338
  不需要 clone 仓库或寻找 examples 文件,也能快速看到 ProofPR 会抓什么风险。
26028
26339
 
26029
- 7. 查看所有内置案例
26340
+ 8. 查看所有内置案例
26030
26341
  npx proof-pr@latest demo --list
26031
26342
 
26032
- 8. 验证规则样本是否仍然命中
26343
+ 9. 验证规则样本是否仍然命中
26033
26344
  npx proof-pr@latest benchmark --cases benchmarks/cases
26034
26345
  适合维护 ProofPR 规则或发版前回归。
26035
26346
 
26036
- 9. 调整审查强度
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-pr",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "CLI for ProofPR, a maintainer-focused pull request evidence scanner.",
5
5
  "license": "MIT",
6
6
  "type": "module",