From 45970f60133d7d32dbc778c367520fa9c449c2d0 Mon Sep 17 00:00:00 2001 From: farmyobutu5233 Date: Sun, 5 Jul 2026 10:03:02 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat(research):=20=E6=96=B0=E5=A2=9E=20repr?= =?UTF-8?q?o-audit=20=E7=A7=91=E7=A0=94=E5=A4=8D=E7=8E=B0=E6=80=A7?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=EF=BC=88Skill=20+=20=E5=B7=A5=E5=85=B7=20+?= =?UTF-8?q?=20=E7=9C=9F=E5=AE=9E=E9=AA=8C=E8=AF=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- examples/research/repro-audit/README.md | 56 +++++ .../research/repro-audit/docs/verification.md | 35 +++ .../repro-audit-GengzhaoWang-UAV-Paper.md | 17 ++ .../repro-audit-momomym-paper-summarizer.md | 17 ++ .../__pycache__/repro_audit.cpython-312.pyc | Bin 0 -> 14704 bytes .../repro-audit/scripts/repro_audit.py | 205 ++++++++++++++++++ .../research/repro-audit/tests/test_audit.py | 73 +++++++ skills/gitlink-repro-audit/SKILL.md | 69 ++++++ 8 files changed, 472 insertions(+) create mode 100644 examples/research/repro-audit/README.md create mode 100644 examples/research/repro-audit/docs/verification.md create mode 100644 examples/research/repro-audit/examples/demo-outputs/repro-audit-GengzhaoWang-UAV-Paper.md create mode 100644 examples/research/repro-audit/examples/demo-outputs/repro-audit-momomym-paper-summarizer.md create mode 100644 examples/research/repro-audit/scripts/__pycache__/repro_audit.cpython-312.pyc create mode 100644 examples/research/repro-audit/scripts/repro_audit.py create mode 100644 examples/research/repro-audit/tests/test_audit.py create mode 100644 skills/gitlink-repro-audit/SKILL.md diff --git a/examples/research/repro-audit/README.md b/examples/research/repro-audit/README.md new file mode 100644 index 0000000..48e3c16 --- /dev/null +++ b/examples/research/repro-audit/README.md @@ -0,0 +1,56 @@ +# 科研项目复现性审计(repro-audit) + +把 [`gitlink-repro-audit` Skill](../../../skills/gitlink-repro-audit/SKILL.md) 包成**可直接运行的科研辅助工具**(子赛题四:应用 GitLink 辅助科研): + +> **采集 → 评估 → 报告 → 回写**:用 `repo +tree` / `file +view --raw` / `release +list` 采集仓库结构、README 与发布信息,做八维**确定性复现性检查**(文档 / 许可证 / 引用 / 依赖固化 / 运行入口 / 数据说明 / 测试 / 版本固化),输出 0-100 评分卡 + A/B/C/D 等级 + 逐项修复建议,并(仅在 `--apply` 时)用 `issue +create` 把报告作为改进 tracking issue 回写。 + +## 科研赋能价值 + +- **课题组自查**:论文投稿/开源发布前对代码仓库做复现性体检,逐项补齐 +- **学术规范检查**:许可证、引用信息(CITATION.cff/BibTeX)、数据可得性一次性核验 +- **可进 CI**:退出码 `2` 表示存在明显复现缺口,可作为科研仓库的发布门禁 + +## 交付物 + +- `scripts/repro_audit.py`:完整审计闭环(纯标准库,Python ≥3.9,零第三方依赖) +- `tests/test_audit.py`:确定性回归护栏(7/7 全绿,同输入同分同等级) +- `docs/verification.md`:真实科研仓库验证证据 +- `examples/demo-outputs/`:两个真实仓库的审计报告 + +## 快速运行(默认 dry-run,不写远端) + +```bash +npm install -g @gitlink-ai/cli +gitlink-cli auth login + +python3 scripts/repro_audit.py --owner --repo --output-dir outputs +# 回写改进 tracking issue(请先确认报告内容):加 --apply +``` + +## 已在真实科研仓库验证 + +见 [`docs/verification.md`](docs/verification.md): + +| 仓库 | 类型 | 得分 | 等级 | +|------|------|------|------| +| GengzhaoWang/UAV-Paper | 论文笔记仓库 | 8/100 | D(复现困难,符合预期) | +| momomym/paper-summarizer | 科研工具代码仓库 | 58/100 | C(缺引用/数据说明/release,建议明确) | + +两个仓库得分区分度显著,逐项证据均可在仓库页面人工复核。 + +## 评分标准 + +| 检查项 | 分值 | 判据 | +|--------|------|------| +| README 与运行说明 | 15 | README 存在且含运行/复现章节(仅 README 得 8) | +| 开源许可证 | 15 | LICENSE / COPYING | +| 引用信息 | 10 | CITATION.cff 或 README BibTeX | +| 依赖清单 | 15 | requirements.txt / environment.yml / go.mod 等 | +| 运行入口 | 10 | Makefile / run.sh / main.py / Dockerfile / scripts | +| 数据可得性说明 | 10 | data 目录或 README 数据说明 | +| 测试/验证代码 | 10 | tests 目录 | +| 版本发布 | 15 | ≥1 个 release | + +等级:≥85 A / ≥70 B / ≥50 C / <50 D。 + +> 依赖 `file` 快捷命令组(PR #330);在其合并前可用 `--cli` 指向包含该命令的本地构建。 diff --git a/examples/research/repro-audit/docs/verification.md b/examples/research/repro-audit/docs/verification.md new file mode 100644 index 0000000..925c435 --- /dev/null +++ b/examples/research/repro-audit/docs/verification.md @@ -0,0 +1,35 @@ +# 真实平台验证记录 + +验证日期:2026-07-05;平台:生产环境 gitlink.org.cn;CLI:包含 `file` 命令组(PR #330)的本地构建。 + +## 1. 论文笔记仓库(预期低分) + +```bash +python3 scripts/repro_audit.py --owner GengzhaoWang --repo UAV-Paper +``` + +- 结果:**8/100 — D(复现困难)**,退出码 `2` +- 证据:仅有 README(无运行说明章节),无 LICENSE / 引用 / 依赖清单 / 入口 / 数据说明 / 测试 / release +- 完整报告:[`../examples/demo-outputs/repro-audit-GengzhaoWang-UAV-Paper.md`](../examples/demo-outputs/repro-audit-GengzhaoWang-UAV-Paper.md) + +## 2. 科研工具代码仓库(中等分,建议明确) + +```bash +python3 scripts/repro_audit.py --owner momomym --repo paper-summarizer +``` + +- 结果:**58/100 — C(存在明显缺口)**,退出码 `2` +- 检出:LICENSE ✅、requirements.txt ✅、scripts 入口 ✅、tests ✅ +- 缺口:README 无运行章节(部分分 8/15)、无引用信息、无数据说明、无 release +- 完整报告:[`../examples/demo-outputs/repro-audit-momomym-paper-summarizer.md`](../examples/demo-outputs/repro-audit-momomym-paper-summarizer.md) + +两个真实仓库得分区分度显著(8 vs 58),每项证据均可在仓库网页人工复核。 + +## 3. 确定性回归护栏 + +```bash +python3 tests/test_audit.py +# Ran 7 tests ... OK +``` + +覆盖:满分样例(100/A)、空仓库(0/D)、README 无运行说明的部分分、README 内引用识别、scripts 目录作为入口、同输入同输出确定性、失败项必附修复建议。 diff --git a/examples/research/repro-audit/examples/demo-outputs/repro-audit-GengzhaoWang-UAV-Paper.md b/examples/research/repro-audit/examples/demo-outputs/repro-audit-GengzhaoWang-UAV-Paper.md new file mode 100644 index 0000000..682b4e1 --- /dev/null +++ b/examples/research/repro-audit/examples/demo-outputs/repro-audit-GengzhaoWang-UAV-Paper.md @@ -0,0 +1,17 @@ +# 科研复现性审计报告:GengzhaoWang/UAV-Paper(ref: 默认分支) + +**总分:8/100 — 等级 D(复现困难)** + +| 检查项 | 得分 | 证据 | 建议 | +|--------|------|------|------| +| README 与运行说明 | 8/15 | README=README.md;运行/复现章节=未检出 | 补充 README 并加入 How to run / 复现步骤章节 | +| 开源许可证 | 0/15 | 许可证文件=缺失 | 添加 LICENSE 文件(科研代码推荐 MIT/Apache-2.0/BSD) | +| 引用信息 | 0/10 | CITATION 文件=无;README 引用章节=无 | 添加 CITATION.cff 或在 README 中给出 BibTeX | +| 依赖清单(环境固化) | 0/15 | 依赖文件=缺失 | 添加 requirements.txt / environment.yml 等依赖清单并固定版本 | +| 运行入口 | 0/10 | 入口=未检出 | 提供 Makefile / run.sh / main.py 等一键运行入口 | +| 数据可得性说明 | 0/10 | 数据目录=无;README 数据说明=无 | 在 README 中说明数据集来源、获取方式与预处理步骤 | +| 测试/验证代码 | 0/10 | 测试目录=无 | 添加 tests/ 验证关键结果可复算 | +| 版本发布(成果固化) | 0/15 | release 数=0 | 为论文对应的代码状态打 tag 并创建 release | + +--- +*由 repro-audit 生成(确定性检查,同输入同输出)。* \ No newline at end of file diff --git a/examples/research/repro-audit/examples/demo-outputs/repro-audit-momomym-paper-summarizer.md b/examples/research/repro-audit/examples/demo-outputs/repro-audit-momomym-paper-summarizer.md new file mode 100644 index 0000000..6a895ac --- /dev/null +++ b/examples/research/repro-audit/examples/demo-outputs/repro-audit-momomym-paper-summarizer.md @@ -0,0 +1,17 @@ +# 科研复现性审计报告:momomym/paper-summarizer(ref: 默认分支) + +**总分:58/100 — 等级 C(存在明显缺口)** + +| 检查项 | 得分 | 证据 | 建议 | +|--------|------|------|------| +| README 与运行说明 | 8/15 | README=README.md;运行/复现章节=未检出 | 补充 README 并加入 How to run / 复现步骤章节 | +| 开源许可证 | 15/15 | 许可证文件=LICENSE | — | +| 引用信息 | 0/10 | CITATION 文件=无;README 引用章节=无 | 添加 CITATION.cff 或在 README 中给出 BibTeX | +| 依赖清单(环境固化) | 15/15 | 依赖文件=requirements.txt | — | +| 运行入口 | 10/10 | 入口=scripts | — | +| 数据可得性说明 | 0/10 | 数据目录=无;README 数据说明=无 | 在 README 中说明数据集来源、获取方式与预处理步骤 | +| 测试/验证代码 | 10/10 | 测试目录=tests | — | +| 版本发布(成果固化) | 0/15 | release 数=0 | 为论文对应的代码状态打 tag 并创建 release | + +--- +*由 repro-audit 生成(确定性检查,同输入同输出)。* \ No newline at end of file diff --git a/examples/research/repro-audit/scripts/__pycache__/repro_audit.cpython-312.pyc b/examples/research/repro-audit/scripts/__pycache__/repro_audit.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2380313f6eaf4c89ee605a093e526e974b275af7 GIT binary patch literal 14704 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%0AwvnFe;cn07&&FlwxJsSe4&{gY8W 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 literal 0 HcmV?d00001 diff --git a/examples/research/repro-audit/scripts/repro_audit.py b/examples/research/repro-audit/scripts/repro_audit.py new file mode 100644 index 0000000..8f92024 --- /dev/null +++ b/examples/research/repro-audit/scripts/repro_audit.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""科研项目复现性审计(repro-audit)。 + +面向科研代码仓库的确定性复现性评估: +1. 采集:`repo +tree` 获取仓库结构,`file +view --raw` 拉取 README,`release +list` 查发布 +2. 评估:八个维度的复现性检查项(文档/许可证/引用/依赖/入口/数据说明/测试/版本固化) +3. 报告:0-100 评分卡 + A/B/C/D 等级 + 逐项修复建议 +4. 回写:--apply 时用 `issue +create` 把报告作为改进 tracking issue 提交 + +纯标准库实现(Python >= 3.9),gitlink-cli 为唯一外部依赖。默认 dry-run。 +""" + +import argparse +import json +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class CheckResult: + name: str + weight: int + score: int # 0..weight + evidence: str + advice: str + + +def run_cli(args, cli="gitlink-cli"): + result = subprocess.run([cli, *args], capture_output=True, text=True, check=False) + if result.returncode != 0: + raise RuntimeError( + f"gitlink-cli {' '.join(args)} 失败: {result.stderr.strip() or result.stdout.strip()}" + ) + return result.stdout + + +def get_json(args, cli="gitlink-cli"): + payload = json.loads(run_cli([*args, "--format", "json"], cli=cli)) + return payload.get("data", payload) + + +def list_entries(owner, repo, ref, path="", cli="gitlink-cli"): + args = ["repo", "+tree", "--owner", owner, "--repo", repo] + if ref: + args += ["--ref", ref] + if path: + args += ["--path", path] + data = get_json(args, cli=cli) + entries = data.get("entries", data) if isinstance(data, dict) else data + out = [] + if isinstance(entries, list): + for e in entries: + if isinstance(e, dict) and e.get("name"): + out.append((e["name"], e.get("type", ""))) + return out + + +def fetch_readme(owner, repo, ref, names, cli="gitlink-cli"): + for name in names: + if re.match(r"(?i)^readme(\.|$)", name): + args = ["file", "+view", "--owner", owner, "--repo", repo, "--path", name, "--raw"] + if ref: + args += ["--ref", ref] + try: + return name, run_cli(args, cli=cli) + except RuntimeError: + continue + return None, "" + + +def count_releases(owner, repo, cli="gitlink-cli"): + try: + data = get_json(["release", "+list", "--owner", owner, "--repo", repo], cli=cli) + except RuntimeError: + return 0 + releases = data.get("releases", data) if isinstance(data, dict) else data + return len(releases) if isinstance(releases, list) else 0 + + +DEP_FILES = [ + "requirements.txt", "environment.yml", "environment.yaml", "Pipfile", + "pyproject.toml", "setup.py", "go.mod", "package.json", "Cargo.toml", + "pom.xml", "build.gradle", "CMakeLists.txt", "DESCRIPTION", "renv.lock", +] +ENTRY_FILES = ["Makefile", "makefile", "run.sh", "train.sh", "main.py", "run.py", "train.py", "Dockerfile", "docker-compose.yml"] +CITATION_FILES = ["CITATION.cff", "CITATION", "CITATION.bib", "citation.bib"] +LICENSE_RE = re.compile(r"(?i)^(license|licence|copying)(\.|$)") +DATA_HINT_RE = re.compile(r"(?i)\b(dataset|data set|数据集|数据来源|download.*data|data/)\b") +REPRO_HINT_RE = re.compile(r"(?i)(reproduc|复现|实验设置|experiment setup|how to run|usage|快速开始|quick\s?start)") + + +def audit(names, dirs, readme_text, release_count): + """确定性打分:names=顶层文件名,dirs=顶层目录名。返回 CheckResult 列表。""" + nameset = set(names) + results = [] + + def add(name, weight, ok, evidence, advice, partial=None): + score = weight if ok else (partial if partial else 0) + results.append(CheckResult(name, weight, score, evidence, "" if ok else advice)) + + readme_name = next((n for n in names if re.match(r"(?i)^readme(\.|$)", n)), None) + has_usage = bool(readme_text and REPRO_HINT_RE.search(readme_text)) + add("README 与运行说明", 15, bool(readme_name and has_usage), + f"README={readme_name or '缺失'};运行/复现章节={'有' if has_usage else '未检出'}", + "补充 README 并加入 How to run / 复现步骤章节", + partial=8 if readme_name else 0) + + lic = next((n for n in names if LICENSE_RE.match(n)), None) + add("开源许可证", 15, bool(lic), f"许可证文件={lic or '缺失'}", + "添加 LICENSE 文件(科研代码推荐 MIT/Apache-2.0/BSD)") + + cit = next((n for n in names if n in CITATION_FILES), None) + cite_in_readme = bool(readme_text and re.search(r"(?i)(citation|引用|bibtex|@(article|inproceedings))", readme_text)) + add("引用信息", 10, bool(cit or cite_in_readme), + f"CITATION 文件={cit or '无'};README 引用章节={'有' if cite_in_readme else '无'}", + "添加 CITATION.cff 或在 README 中给出 BibTeX") + + dep = next((n for n in names if n in DEP_FILES), None) + add("依赖清单(环境固化)", 15, bool(dep), f"依赖文件={dep or '缺失'}", + "添加 requirements.txt / environment.yml 等依赖清单并固定版本") + + entry = next((n for n in names if n in ENTRY_FILES), None) + script_dir = next((d for d in dirs if d in ("scripts", "bin")), None) + add("运行入口", 10, bool(entry or script_dir), + f"入口={entry or script_dir or '未检出'}", + "提供 Makefile / run.sh / main.py 等一键运行入口") + + data_dir = next((d for d in dirs if re.match(r"(?i)^(data|datasets?)$", d)), None) + data_in_readme = bool(readme_text and DATA_HINT_RE.search(readme_text)) + add("数据可得性说明", 10, bool(data_dir or data_in_readme), + f"数据目录={data_dir or '无'};README 数据说明={'有' if data_in_readme else '无'}", + "在 README 中说明数据集来源、获取方式与预处理步骤") + + test_dir = next((d for d in dirs if re.match(r"(?i)^tests?$", d)), None) + add("测试/验证代码", 10, bool(test_dir), f"测试目录={test_dir or '无'}", + "添加 tests/ 验证关键结果可复算") + + add("版本发布(成果固化)", 15, release_count > 0, f"release 数={release_count}", + "为论文对应的代码状态打 tag 并创建 release") + + return results + + +GRADE = [(85, "A(可复现性良好)"), (70, "B(基本可复现)"), (50, "C(存在明显缺口)"), (0, "D(复现困难)")] + + +def render_report(owner, repo, ref, results): + total = sum(r.score for r in results) + grade = next(g for t, g in GRADE if total >= t) + lines = [ + f"# 科研复现性审计报告:{owner}/{repo}(ref: {ref or '默认分支'})", + "", + f"**总分:{total}/100 — 等级 {grade}**", + "", + "| 检查项 | 得分 | 证据 | 建议 |", + "|--------|------|------|------|", + ] + for r in results: + lines.append(f"| {r.name} | {r.score}/{r.weight} | {r.evidence} | {r.advice or '—'} |") + lines += ["", "---", "*由 repro-audit 生成(确定性检查,同输入同输出)。*"] + return "\n".join(lines), total + + +def main(): + parser = argparse.ArgumentParser(description="科研项目复现性审计") + parser.add_argument("--owner", required=True) + parser.add_argument("--repo", required=True) + parser.add_argument("--ref", default="") + parser.add_argument("--apply", action="store_true", help="把报告作为 tracking issue 回写(默认 dry-run)") + parser.add_argument("--output-dir", default="outputs") + parser.add_argument("--cli", default="gitlink-cli") + args = parser.parse_args() + + entries = list_entries(args.owner, args.repo, args.ref, cli=args.cli) + names = [n for n, t in entries if t != "dir"] + dirs = [n for n, t in entries if t == "dir"] + _, readme_text = fetch_readme(args.owner, args.repo, args.ref, names, cli=args.cli) + releases = count_releases(args.owner, args.repo, cli=args.cli) + + results = audit(names, dirs, readme_text, releases) + report, total = render_report(args.owner, args.repo, args.ref, results) + + out_dir = Path(args.output_dir) + out_dir.mkdir(parents=True, exist_ok=True) + path = out_dir / f"repro-audit-{args.owner}-{args.repo}.md" + path.write_text(report, encoding="utf-8") + print(report) + print(f"\n报告已保存:{path}", file=sys.stderr) + + if args.apply: + run_cli([ + "issue", "+create", "--owner", args.owner, "--repo", args.repo, + "--title", f"[repro-audit] 复现性审计报告({total}/100)", + "--body", report, "--format", "json", + ], cli=args.cli) + print("已创建 tracking issue。", file=sys.stderr) + + return 0 if total >= 70 else 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/research/repro-audit/tests/test_audit.py b/examples/research/repro-audit/tests/test_audit.py new file mode 100644 index 0000000..2143e23 --- /dev/null +++ b/examples/research/repro-audit/tests/test_audit.py @@ -0,0 +1,73 @@ +"""确定性回归护栏:同输入 → 同分 → 同等级。""" + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) + +from repro_audit import audit, render_report # noqa: E402 + +GOOD_README = """# Project + +## How to run + +```bash +make train +``` + +## Dataset + +Download the dataset from ... + +## Citation + +```bibtex +@article{x2026} +``` +""" + + +class AuditTest(unittest.TestCase): + def test_full_marks(self): + names = ["README.md", "LICENSE", "CITATION.cff", "requirements.txt", "Makefile"] + dirs = ["data", "tests", "src"] + results = audit(names, dirs, GOOD_README, release_count=2) + report, total = render_report("o", "r", "", results) + self.assertEqual(total, 100) + self.assertIn("等级 A", report) + + def test_empty_repo(self): + results = audit([], [], "", release_count=0) + report, total = render_report("o", "r", "", results) + self.assertEqual(total, 0) + self.assertIn("等级 D", report) + + def test_readme_without_usage_partial(self): + results = audit(["README.md"], [], "# hi", release_count=0) + readme = next(r for r in results if r.name == "README 与运行说明") + self.assertEqual(readme.score, 8) # 有 README 无运行说明 → 部分分 + + def test_citation_in_readme_counts(self): + results = audit(["README.md"], [], "## Citation\n@article{x}", release_count=0) + cit = next(r for r in results if r.name == "引用信息") + self.assertEqual(cit.score, cit.weight) + + def test_scripts_dir_as_entry(self): + results = audit([], ["scripts"], "", release_count=0) + entry = next(r for r in results if r.name == "运行入口") + self.assertEqual(entry.score, entry.weight) + + def test_deterministic(self): + a = audit(["README.md", "go.mod"], ["tests"], GOOD_README, 1) + b = audit(["README.md", "go.mod"], ["tests"], GOOD_README, 1) + self.assertEqual([vars(x) for x in a], [vars(y) for y in b]) + + def test_advice_present_for_failures(self): + results = audit([], [], "", 0) + for r in results: + self.assertTrue(r.advice, f"{r.name} 应给出修复建议") + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/gitlink-repro-audit/SKILL.md b/skills/gitlink-repro-audit/SKILL.md new file mode 100644 index 0000000..3e2f845 --- /dev/null +++ b/skills/gitlink-repro-audit/SKILL.md @@ -0,0 +1,69 @@ +--- +name: gitlink-repro-audit +version: 1.0.0 +description: "科研项目复现性审计:对 GitLink 上的科研代码仓库做八维复现性检查(文档/许可证/引用/依赖/入口/数据说明/测试/版本固化),生成 0-100 评分卡与修复建议,并可回写改进 tracking issue。当用户需要评估科研仓库可复现性、准备论文代码发布、或做学术规范检查时触发。" +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli repo --help" +--- + +# gitlink-repro-audit(科研复现性审计) + +**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。** +**CRITICAL — 审计本身只读;`--apply` 回写 tracking issue 前必须先向用户展示报告并确认。** +**CRITICAL — GitLink 操作只能用 `gitlink-cli`。** + +> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) + +--- + +## 功能概述 + +论文代码「跑不起来」是科研复现危机的核心。本技能对科研代码仓库做系统化复现性评估: + +| 检查项 | 分值 | 判据 | +|--------|------|------| +| README 与运行说明 | 15 | 存在 README 且含 How to run / 复现步骤章节(仅有 README 得 8 分) | +| 开源许可证 | 15 | LICENSE / COPYING 文件 | +| 引用信息 | 10 | CITATION.cff 或 README 中的 BibTeX/引用章节 | +| 依赖清单(环境固化) | 15 | requirements.txt / environment.yml / go.mod / package.json 等 | +| 运行入口 | 10 | Makefile / run.sh / main.py / Dockerfile / scripts 目录 | +| 数据可得性说明 | 10 | data 目录或 README 中的数据集来源说明 | +| 测试/验证代码 | 10 | tests 目录 | +| 版本发布(成果固化) | 15 | 至少一个 release | + +总分映射等级:≥85 A(可复现性良好)/ ≥70 B / ≥50 C / <50 D(复现困难)。 + +## 使用方式 + +### 方式一:确定性脚本(推荐,可进 CI) + +```bash +python3 examples/research/repro-audit/scripts/repro_audit.py \ + --owner --repo [--ref ] [--apply] +``` + +- 默认 dry-run 只输出报告;`--apply` 用 `issue +create` 回写改进 tracking issue +- 退出码:`0` 总分 ≥70(基本可复现),`2` 存在明显缺口 + +### 方式二:AI Agent 手工执行命令链 + +```bash +# 1. 结构采集 +gitlink-cli repo +tree --owner --repo --format json +# 2. README 内容 +gitlink-cli file +view --owner --repo --path README.md --raw +# 3. 版本发布 +gitlink-cli release +list --owner --repo --format json +# 4.(确认后)回写改进 issue +gitlink-cli issue +create --owner --repo -t "[repro-audit] 复现性审计报告" -b "<报告>" +``` + +AI 按上表逐项打分,报告须给出每项证据与修复建议,不允许笼统结论。 + +## 注意事项 + +- 判据基于仓库顶层结构与 README 文本,属于必要条件检查:高分不代表结果一定可复现,低分则一定存在工程缺口。 +- 对论文笔记类仓库(纯 Markdown)等级普遍偏低,属预期行为;报告建议仅在其确为「实验代码仓库」时回写。 +- 与 [`../gitlink-doc-sync/SKILL.md`](../gitlink-doc-sync/SKILL.md)(文档一致性)、[`../gitlink-license-compliance/SKILL.md`](../gitlink-license-compliance/SKILL.md)(许可证合规)互补:本技能聚焦复现性维度的整体评估。 From 3b017d3cb67cea802ffdbb63c52be2c3a8454ed6 Mon Sep 17 00:00:00 2001 From: farmyobutu5233 Date: Sun, 5 Jul 2026 10:44:13 +0000 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20=E7=A7=BB=E9=99=A4=E8=AF=AF?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E7=9A=84=20=5F=5Fpycache=5F=5F=EF=BC=9Bdocs(?= =?UTF-8?q?research):=20=E8=A1=A5=E5=85=85=E6=9E=B6=E6=9E=84=E5=9B=BE?= =?UTF-8?q?=E4=B8=8E=E7=A7=91=E7=A0=94=E5=9C=BA=E6=99=AF=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitignore | 1 + examples/research/repro-audit/README.md | 12 ++++++ .../repro-audit/docs/application-report.md | 37 ++++++++++++++++++ .../__pycache__/repro_audit.cpython-312.pyc | Bin 14704 -> 0 bytes 4 files changed, 50 insertions(+) create mode 100644 examples/research/repro-audit/docs/application-report.md delete mode 100644 examples/research/repro-audit/scripts/__pycache__/repro_audit.cpython-312.pyc diff --git a/.gitignore b/.gitignore index bd0ccf8..c80facc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ gitlink-cli.exe /gitlink-cli +__pycache__/ diff --git a/examples/research/repro-audit/README.md b/examples/research/repro-audit/README.md index 48e3c16..5d002cc 100644 --- a/examples/research/repro-audit/README.md +++ b/examples/research/repro-audit/README.md @@ -10,6 +10,18 @@ - **学术规范检查**:许可证、引用信息(CITATION.cff/BibTeX)、数据可得性一次性核验 - **可进 CI**:退出码 `2` 表示存在明显复现缺口,可作为科研仓库的发布门禁 +## 架构图 + +```mermaid +flowchart LR + A["repo +tree
仓库顶层结构"] --> D["八维确定性评估
文档/许可证/引用/依赖
入口/数据说明/测试/版本固化"] + B["file +view --raw
README 内容"] --> D + C["release +list
版本发布信息"] --> D + D --> E["0-100 评分卡
A/B/C/D 等级 + 逐项证据与建议"] + E -->|"dry-run(默认)"| F["报告落盘
退出码 0/2 → 发布门禁"] + E -->|"--apply"| G["issue +create
回写改进 tracking issue"] +``` + ## 交付物 - `scripts/repro_audit.py`:完整审计闭环(纯标准库,Python ≥3.9,零第三方依赖) diff --git a/examples/research/repro-audit/docs/application-report.md b/examples/research/repro-audit/docs/application-report.md new file mode 100644 index 0000000..5defad3 --- /dev/null +++ b/examples/research/repro-audit/docs/application-report.md @@ -0,0 +1,37 @@ +# 科研场景应用报告:基于 gitlink-cli 的科研复现性审计 + +## 1. 科研问题背景 + +「复现危机」是学术界公认的系统性问题:大量论文附带的代码仓库缺少依赖清单、运行说明、数据来源与版本固化,导致同行无法复现结果。科研工作者需要一个**低成本、可自动化、判据透明**的工具,在论文投稿与代码开源前完成复现性体检。 + +## 2. 技术实现 + +- **数据获取层**:全部通过 `gitlink-cli` 完成——`repo +tree`(仓库结构)、`file +view --raw`(README 内容)、`release +list`(版本发布),无任何直接 HTTP 调用。 +- **评估层**:八维确定性检查(README 运行说明 / 许可证 / 引用信息 / 依赖固化 / 运行入口 / 数据可得性 / 测试 / 版本发布),每项有明确判据、分值与证据输出;同输入必同输出,可进 CI。 +- **输出层**:0-100 评分卡 + A/B/C/D 等级 + 逐项修复建议;`--apply` 时用 `issue +create` 把报告回写为改进 tracking issue,形成可追踪的整改闭环。 +- **工程质量**:纯 Python 标准库(≥3.9)零第三方依赖;7 个确定性单测护栏;默认 dry-run,写操作显式开启。 + +## 3. 科研赋能价值 + +| 使用者 | 场景 | 价值 | +|--------|------|------| +| 课题组 | 论文投稿/开源发布前自查 | 逐项补齐复现要件,提升论文可信度 | +| 实验室管理者 | 批量审计组内科研仓库 | 统一学术规范(许可证/引用/数据说明) | +| 期刊/会议 artifact 评审 | 快速初筛 | 评分卡作为客观初审依据 | +| CI 门禁 | 科研仓库发布流程 | 退出码 2 阻断复现缺口明显的发布 | + +## 4. 落地效果(生产环境实测) + +在 gitlink.org.cn 生产环境对真实科研类仓库验证: + +| 仓库 | 类型 | 得分 | 等级 | 主要缺口 | +|------|------|------|------|----------| +| GengzhaoWang/UAV-Paper | 论文笔记仓库 | 8/100 | D | 全维度缺失(符合预期) | +| momomym/paper-summarizer | 科研工具代码仓库 | 58/100 | C | 引用信息、数据说明、release | + +区分度显著且每项证据可在仓库页面人工复核;`--apply` 回写 tracking issue 已在自有 fork 实测成功。完整证据见 [`verification.md`](verification.md),演示输出见 [`../examples/demo-outputs/`](../examples/demo-outputs/)。 + +## 5. 与 Skills 生态的关系 + +- 判据与命令链沉淀为 [`gitlink-repro-audit` Skill](../../../../skills/gitlink-repro-audit/SKILL.md),兼容 Claude Code / Cursor 等主流 AI Agent(标准 SKILL.md 规范)。 +- 与 `gitlink-doc-sync`(文档一致性)、`gitlink-license-compliance`(许可证合规)互补,共同覆盖科研仓库的学术规范维度。 diff --git a/examples/research/repro-audit/scripts/__pycache__/repro_audit.cpython-312.pyc b/examples/research/repro-audit/scripts/__pycache__/repro_audit.cpython-312.pyc deleted file mode 100644 index 2380313f6eaf4c89ee605a093e526e974b275af7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14704 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%0AwvnFe;cn07&&FlwxJsSe4&{gY8W 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 From c1a0e4cc61bbdff595f3a694bb70cddc15155ded Mon Sep 17 00:00:00 2001 From: farmyobutu5233 Date: Sun, 5 Jul 2026 11:03:00 +0000 Subject: [PATCH 3/4] =?UTF-8?q?feat(repro-audit):=20=E6=96=B0=E5=A2=9E=20-?= =?UTF-8?q?-repos-file=20=E6=89=B9=E9=87=8F=E5=AE=A1=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E4=B8=8E=E7=A1=AE=E5=AE=9A=E6=80=A7=E6=B1=87=E6=80=BB?= =?UTF-8?q?=E6=8E=92=E5=90=8D=EF=BC=8C=E5=8D=95=E6=B5=8B=2010/10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- examples/research/repro-audit/README.md | 4 + .../repro-audit/docs/application-report.md | 2 +- .../repro-audit/scripts/repro_audit.py | 91 ++++++++++++++----- .../research/repro-audit/tests/test_audit.py | 23 ++++- 4 files changed, 97 insertions(+), 23 deletions(-) diff --git a/examples/research/repro-audit/README.md b/examples/research/repro-audit/README.md index 5d002cc..ea1fbe4 100644 --- a/examples/research/repro-audit/README.md +++ b/examples/research/repro-audit/README.md @@ -37,6 +37,10 @@ gitlink-cli auth login python3 scripts/repro_audit.py --owner --repo --output-dir outputs # 回写改进 tracking issue(请先确认报告内容):加 --apply + +# 批量审计(实验室/课题组场景):清单每行 owner/repo,# 为注释 +python3 scripts/repro_audit.py --repos-file repos.txt --output-dir outputs +# 输出逐仓库报告 + repro-audit-summary.md 汇总排名(得分降序,退出码 2 表示存在 <70 分仓库) ``` ## 已在真实科研仓库验证 diff --git a/examples/research/repro-audit/docs/application-report.md b/examples/research/repro-audit/docs/application-report.md index 5defad3..091d437 100644 --- a/examples/research/repro-audit/docs/application-report.md +++ b/examples/research/repro-audit/docs/application-report.md @@ -16,7 +16,7 @@ | 使用者 | 场景 | 价值 | |--------|------|------| | 课题组 | 论文投稿/开源发布前自查 | 逐项补齐复现要件,提升论文可信度 | -| 实验室管理者 | 批量审计组内科研仓库 | 统一学术规范(许可证/引用/数据说明) | +| 实验室管理者 | 批量审计组内科研仓库(`--repos-file` 清单模式,输出得分排名汇总表) | 统一学术规范(许可证/引用/数据说明) | | 期刊/会议 artifact 评审 | 快速初筛 | 评分卡作为客观初审依据 | | CI 门禁 | 科研仓库发布流程 | 退出码 2 阻断复现缺口明显的发布 | diff --git a/examples/research/repro-audit/scripts/repro_audit.py b/examples/research/repro-audit/scripts/repro_audit.py index 8f92024..a0dacfd 100644 --- a/examples/research/repro-audit/scripts/repro_audit.py +++ b/examples/research/repro-audit/scripts/repro_audit.py @@ -164,40 +164,89 @@ def render_report(owner, repo, ref, results): return "\n".join(lines), total +def audit_repo(owner, repo, ref, out_dir, cli, apply_issue=False, echo=True): + """审计单个仓库,落盘报告,返回总分。""" + entries = list_entries(owner, repo, ref, cli=cli) + names = [n for n, t in entries if t != "dir"] + dirs = [n for n, t in entries if t == "dir"] + _, readme_text = fetch_readme(owner, repo, ref, names, cli=cli) + releases = count_releases(owner, repo, cli=cli) + + results = audit(names, dirs, readme_text, releases) + report, total = render_report(owner, repo, ref, results) + + path = out_dir / f"repro-audit-{owner}-{repo}.md" + path.write_text(report, encoding="utf-8") + if echo: + print(report) + print(f"\n报告已保存:{path}", file=sys.stderr) + + if apply_issue: + run_cli([ + "issue", "+create", "--owner", owner, "--repo", repo, + "--title", f"[repro-audit] 复现性审计报告({total}/100)", + "--body", report, "--format", "json", + ], cli=cli) + print("已创建 tracking issue。", file=sys.stderr) + return total + + +def read_repos_file(path): + """读取批量仓库清单:每行 owner/repo,# 开头为注释。""" + repos = [] + for line in Path(path).read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + owner, _, repo = line.partition("/") + if not owner or not repo: + raise ValueError(f"无效的仓库行(应为 owner/repo):{line}") + repos.append((owner, repo)) + return repos + + +def render_summary(rows): + """批量审计汇总表(确定性:按得分降序、同分按名称)。""" + rows = sorted(rows, key=lambda r: (-r[1], r[0])) + lines = ["# 批量复现性审计汇总", "", "| 仓库 | 得分 | 等级 |", "|------|------|------|"] + for name, total in rows: + grade = next(g for t, g in GRADE if total >= t) + lines.append(f"| {name} | {total}/100 | {grade} |") + return "\n".join(lines) + "\n" + + def main(): parser = argparse.ArgumentParser(description="科研项目复现性审计") - parser.add_argument("--owner", required=True) - parser.add_argument("--repo", required=True) + parser.add_argument("--owner") + parser.add_argument("--repo") + parser.add_argument("--repos-file", help="批量审计清单文件(每行 owner/repo,# 注释)") parser.add_argument("--ref", default="") parser.add_argument("--apply", action="store_true", help="把报告作为 tracking issue 回写(默认 dry-run)") parser.add_argument("--output-dir", default="outputs") parser.add_argument("--cli", default="gitlink-cli") args = parser.parse_args() - entries = list_entries(args.owner, args.repo, args.ref, cli=args.cli) - names = [n for n, t in entries if t != "dir"] - dirs = [n for n, t in entries if t == "dir"] - _, readme_text = fetch_readme(args.owner, args.repo, args.ref, names, cli=args.cli) - releases = count_releases(args.owner, args.repo, cli=args.cli) - - results = audit(names, dirs, readme_text, releases) - report, total = render_report(args.owner, args.repo, args.ref, results) + if not args.repos_file and not (args.owner and args.repo): + parser.error("需要 --owner 与 --repo,或 --repos-file") out_dir = Path(args.output_dir) out_dir.mkdir(parents=True, exist_ok=True) - path = out_dir / f"repro-audit-{args.owner}-{args.repo}.md" - path.write_text(report, encoding="utf-8") - print(report) - print(f"\n报告已保存:{path}", file=sys.stderr) - if args.apply: - run_cli([ - "issue", "+create", "--owner", args.owner, "--repo", args.repo, - "--title", f"[repro-audit] 复现性审计报告({total}/100)", - "--body", report, "--format", "json", - ], cli=args.cli) - print("已创建 tracking issue。", file=sys.stderr) + if args.repos_file: + rows = [] + for owner, repo in read_repos_file(args.repos_file): + total = audit_repo(owner, repo, args.ref, out_dir, args.cli, + apply_issue=args.apply, echo=False) + rows.append((f"{owner}/{repo}", total)) + summary = render_summary(rows) + summary_path = out_dir / "repro-audit-summary.md" + summary_path.write_text(summary, encoding="utf-8") + print(summary) + print(f"汇总已保存:{summary_path}", file=sys.stderr) + return 0 if all(t >= 70 for _, t in rows) else 2 + total = audit_repo(args.owner, args.repo, args.ref, out_dir, args.cli, + apply_issue=args.apply) return 0 if total >= 70 else 2 diff --git a/examples/research/repro-audit/tests/test_audit.py b/examples/research/repro-audit/tests/test_audit.py index 2143e23..762127f 100644 --- a/examples/research/repro-audit/tests/test_audit.py +++ b/examples/research/repro-audit/tests/test_audit.py @@ -1,12 +1,13 @@ """确定性回归护栏:同输入 → 同分 → 同等级。""" import sys +import tempfile import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) -from repro_audit import audit, render_report # noqa: E402 +from repro_audit import audit, read_repos_file, render_report, render_summary # noqa: E402 GOOD_README = """# Project @@ -68,6 +69,26 @@ class AuditTest(unittest.TestCase): for r in results: self.assertTrue(r.advice, f"{r.name} 应给出修复建议") + def test_read_repos_file(self): + with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False, encoding="utf-8") as f: + f.write("# 注释\n\nowner1/repo1\n owner2/repo2 \n") + path = f.name + self.assertEqual(read_repos_file(path), [("owner1", "repo1"), ("owner2", "repo2")]) + + def test_read_repos_file_invalid_line(self): + with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False, encoding="utf-8") as f: + f.write("not-a-repo-line\n") + path = f.name + with self.assertRaises(ValueError): + read_repos_file(path) + + def test_render_summary_sorted(self): + summary = render_summary([("o/low", 8), ("o/high", 92), ("o/mid", 58)]) + rows = [line for line in summary.splitlines() if line.startswith("| o/")] + self.assertEqual([r.split(" | ")[0] for r in rows], ["| o/high", "| o/mid", "| o/low"]) + self.assertIn("A(可复现性良好)", rows[0]) + self.assertIn("D(复现困难)", rows[2]) + if __name__ == "__main__": unittest.main() From 5089874cd6e06a3e5923e2dd7385a7014cd1abd2 Mon Sep 17 00:00:00 2001 From: farmyobutu5233 Date: Sun, 5 Jul 2026 12:25:02 +0000 Subject: [PATCH 4/4] =?UTF-8?q?feat(repro-audit):=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=8D=95=E4=BB=93=E5=A4=B1=E8=B4=A5=E5=AE=B9?= =?UTF-8?q?=E9=94=99=E5=B9=B6=E5=88=97=E5=85=A5=E6=B1=87=E6=80=BB=EF=BC=9B?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=205=20=E4=BB=93=E5=BA=93=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E6=89=B9=E9=87=8F=20demo=20=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../examples/demo-outputs/repos-batch.txt | 6 +++++ ...epro-audit-steph1-Awesome-Paper-Crawler.md | 17 +++++++++++++ .../demo-outputs/repro-audit-summary.md | 12 ++++++++++ .../repro-audit/scripts/repro_audit.py | 24 ++++++++++++------- .../research/repro-audit/tests/test_audit.py | 5 ++++ 5 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 examples/research/repro-audit/examples/demo-outputs/repos-batch.txt create mode 100644 examples/research/repro-audit/examples/demo-outputs/repro-audit-steph1-Awesome-Paper-Crawler.md create mode 100644 examples/research/repro-audit/examples/demo-outputs/repro-audit-summary.md diff --git a/examples/research/repro-audit/examples/demo-outputs/repos-batch.txt b/examples/research/repro-audit/examples/demo-outputs/repos-batch.txt new file mode 100644 index 0000000..84f20b1 --- /dev/null +++ b/examples/research/repro-audit/examples/demo-outputs/repos-batch.txt @@ -0,0 +1,6 @@ +# 真实科研相关仓库批量复现性审计清单 +GengzhaoWang/UAV-Paper +momomym/paper-summarizer +zhaoyihan/paper-data-redundancy +steph1/Awesome-Paper-Crawler +datawhalechina/paper-chart-tutorial diff --git a/examples/research/repro-audit/examples/demo-outputs/repro-audit-steph1-Awesome-Paper-Crawler.md b/examples/research/repro-audit/examples/demo-outputs/repro-audit-steph1-Awesome-Paper-Crawler.md new file mode 100644 index 0000000..8d99e1d --- /dev/null +++ b/examples/research/repro-audit/examples/demo-outputs/repro-audit-steph1-Awesome-Paper-Crawler.md @@ -0,0 +1,17 @@ +# 科研复现性审计报告:steph1/Awesome-Paper-Crawler(ref: 默认分支) + +**总分:8/100 — 等级 D(复现困难)** + +| 检查项 | 得分 | 证据 | 建议 | +|--------|------|------|------| +| README 与运行说明 | 8/15 | README=README.md;运行/复现章节=未检出 | 补充 README 并加入 How to run / 复现步骤章节 | +| 开源许可证 | 0/15 | 许可证文件=缺失 | 添加 LICENSE 文件(科研代码推荐 MIT/Apache-2.0/BSD) | +| 引用信息 | 0/10 | CITATION 文件=无;README 引用章节=无 | 添加 CITATION.cff 或在 README 中给出 BibTeX | +| 依赖清单(环境固化) | 0/15 | 依赖文件=缺失 | 添加 requirements.txt / environment.yml 等依赖清单并固定版本 | +| 运行入口 | 0/10 | 入口=未检出 | 提供 Makefile / run.sh / main.py 等一键运行入口 | +| 数据可得性说明 | 0/10 | 数据目录=无;README 数据说明=无 | 在 README 中说明数据集来源、获取方式与预处理步骤 | +| 测试/验证代码 | 0/10 | 测试目录=无 | 添加 tests/ 验证关键结果可复算 | +| 版本发布(成果固化) | 0/15 | release 数=0 | 为论文对应的代码状态打 tag 并创建 release | + +--- +*由 repro-audit 生成(确定性检查,同输入同输出)。* \ No newline at end of file diff --git a/examples/research/repro-audit/examples/demo-outputs/repro-audit-summary.md b/examples/research/repro-audit/examples/demo-outputs/repro-audit-summary.md new file mode 100644 index 0000000..233c8cd --- /dev/null +++ b/examples/research/repro-audit/examples/demo-outputs/repro-audit-summary.md @@ -0,0 +1,12 @@ +# 批量复现性审计汇总 + +| 仓库 | 得分 | 等级 | +|------|------|------| +| momomym/paper-summarizer | 58/100 | C(存在明显缺口) | +| GengzhaoWang/UAV-Paper | 8/100 | D(复现困难) | +| steph1/Awesome-Paper-Crawler | 8/100 | D(复现困难) | + +## 无法审计的仓库 + +- datawhalechina/paper-chart-tutorial:gitlink-cli repo +tree --owner datawhalechina --repo paper-chart-tutorial --format json 失败: [-2] 你访问的文件不存在 +- zhaoyihan/paper-data-redundancy:gitlink-cli repo +tree --owner zhaoyihan --repo paper-data-redundancy --format json 失败: [-2] 你访问的文件不存在 diff --git a/examples/research/repro-audit/scripts/repro_audit.py b/examples/research/repro-audit/scripts/repro_audit.py index a0dacfd..baff289 100644 --- a/examples/research/repro-audit/scripts/repro_audit.py +++ b/examples/research/repro-audit/scripts/repro_audit.py @@ -205,13 +205,16 @@ def read_repos_file(path): return repos -def render_summary(rows): - """批量审计汇总表(确定性:按得分降序、同分按名称)。""" +def render_summary(rows, failures=()): + """批量审计汇总表(确定性:按得分降序、同分按名称;失败仓库单独列出)。""" rows = sorted(rows, key=lambda r: (-r[1], r[0])) lines = ["# 批量复现性审计汇总", "", "| 仓库 | 得分 | 等级 |", "|------|------|------|"] for name, total in rows: grade = next(g for t, g in GRADE if total >= t) lines.append(f"| {name} | {total}/100 | {grade} |") + if failures: + lines += ["", "## 无法审计的仓库", ""] + lines += [f"- {name}:{reason}" for name, reason in sorted(failures)] return "\n".join(lines) + "\n" @@ -233,17 +236,22 @@ def main(): out_dir.mkdir(parents=True, exist_ok=True) if args.repos_file: - rows = [] + rows, failures = [], [] for owner, repo in read_repos_file(args.repos_file): - total = audit_repo(owner, repo, args.ref, out_dir, args.cli, - apply_issue=args.apply, echo=False) - rows.append((f"{owner}/{repo}", total)) - summary = render_summary(rows) + name = f"{owner}/{repo}" + try: + total = audit_repo(owner, repo, args.ref, out_dir, args.cli, + apply_issue=args.apply, echo=False) + rows.append((name, total)) + except RuntimeError as exc: + print(f"跳过 {name}:{exc}", file=sys.stderr) + failures.append((name, str(exc))) + summary = render_summary(rows, failures) summary_path = out_dir / "repro-audit-summary.md" summary_path.write_text(summary, encoding="utf-8") print(summary) print(f"汇总已保存:{summary_path}", file=sys.stderr) - return 0 if all(t >= 70 for _, t in rows) else 2 + return 0 if not failures and all(t >= 70 for _, t in rows) else 2 total = audit_repo(args.owner, args.repo, args.ref, out_dir, args.cli, apply_issue=args.apply) diff --git a/examples/research/repro-audit/tests/test_audit.py b/examples/research/repro-audit/tests/test_audit.py index 762127f..85047ae 100644 --- a/examples/research/repro-audit/tests/test_audit.py +++ b/examples/research/repro-audit/tests/test_audit.py @@ -89,6 +89,11 @@ class AuditTest(unittest.TestCase): self.assertIn("A(可复现性良好)", rows[0]) self.assertIn("D(复现困难)", rows[2]) + def test_render_summary_lists_failures(self): + summary = render_summary([("o/ok", 80)], failures=[("o/broken", "仓库树读取失败")]) + self.assertIn("## 无法审计的仓库", summary) + self.assertIn("- o/broken:仓库树读取失败", summary) + if __name__ == "__main__": unittest.main()