From 07071277d524fe44eb78a3787bc739e98852d8a8 Mon Sep 17 00:00:00 2001 From: maidamaliziasimnw Date: Fri, 10 Jul 2026 16:17:40 +0000 Subject: [PATCH] =?UTF-8?q?feat(research):=20=E6=96=B0=E5=A2=9E=E7=A7=91?= =?UTF-8?q?=E7=A0=94=E8=BE=85=E5=8A=A9=E5=BC=80=E5=8F=91=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=20examples/research=EF=BC=88=E8=B5=B7=E6=AD=A5=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=8C=85=E7=BC=BA=E5=A4=B1=E9=A1=B9=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/README.md | 36 ++++++ examples/research/report-template.md | 34 ++++++ .../research_tool_template.cpython-310.pyc | Bin 0 -> 4145 bytes .../scripts/research_tool_template.py | 110 ++++++++++++++++++ .../__pycache__/test_metrics.cpython-310.pyc | Bin 0 -> 3064 bytes examples/research/tests/test_metrics.py | 60 ++++++++++ 6 files changed, 240 insertions(+) create mode 100644 examples/research/README.md create mode 100644 examples/research/report-template.md create mode 100644 examples/research/scripts/__pycache__/research_tool_template.cpython-310.pyc create mode 100644 examples/research/scripts/research_tool_template.py create mode 100644 examples/research/tests/__pycache__/test_metrics.cpython-310.pyc create mode 100644 examples/research/tests/test_metrics.py diff --git a/examples/research/README.md b/examples/research/README.md new file mode 100644 index 0000000..7dd805e --- /dev/null +++ b/examples/research/README.md @@ -0,0 +1,36 @@ +# 科研辅助开发模板(examples/research) + +对应起步资源包「科研辅助开发模板」:科研场景脚本、分析工具、报告生成模板三件套。 +开箱即用(自带一个可运行的示例场景),也可按占位符改造成你自己的科研辅助工具(子赛题四)。 + +## 内容 + +| 文件 | 作用 | +|------|------| +| `scripts/research_tool_template.py` | 科研场景脚本模板:采集(gitlink-cli,自动翻页)→ 指标计算(确定性纯函数)→ 报告渲染 三段式骨架,纯标准库 | +| `report-template.md` | 报告生成模板:`{placeholder}` 占位符由脚本填充,评审可复核的四元组结构(结论/指标/证据/复现命令) | +| `tests/test_metrics.py` | 指标纯函数单测样例(不访问网络) | + +## 快速运行(自带示例:仓库科研活跃度快照) + +```bash +gitlink-cli auth login + +python3 scripts/research_tool_template.py \ + --owner --repo --output report.md +``` + +产出 `report.md`:开放/关闭 issue 数、open PR 数、分支数、近 30 天活跃判定,以及每项指标的复现命令。 + +## 改造为你自己的科研工具(3 步) + +1. **采集**:改 `collect()`——用 `gitlink-cli <命令> --format json` 拉你需要的数据(数据多时记得翻页合并) +2. **指标**:改 `compute_metrics()`——保持**纯函数 + 确定性**(同输入同输出),这样单测和评审复核才可行 +3. **报告**:改 `report-template.md` 的占位符——保留「复现命令」一节,科研结论必须可复现 + +## 设计约定(子赛题四评审要点) + +- **可复现**:报告内嵌每项指标的采集命令,任何人可独立复核 +- **确定性**:指标计算与阈值判定为纯函数,禁止依赖随机/时序副作用(时间基准由 `--now` 注入,便于测试) +- **零依赖**:纯 Python 标准库,无需装包即可在评审机运行 +- 参考实现:更完整的科研复现性审计见 [`skills/gitlink-repro-audit`](../../skills/gitlink-repro-audit/)(若已收录) diff --git a/examples/research/report-template.md b/examples/research/report-template.md new file mode 100644 index 0000000..9d3379e --- /dev/null +++ b/examples/research/report-template.md @@ -0,0 +1,34 @@ +# 科研仓库活跃度快照:{owner}/{repo} + +> 生成时间(UTC):{generated_at} | 工具:examples/research 科研辅助模板 + +## 结论 + +{conclusion} + +## 指标 + +| 指标 | 值 | +|------|-----| +| 开放 issue | {open_issues} | +| 已关闭 issue | {closed_issues} | +| issue 关闭率 | {close_rate} | +| 开放 PR | {open_prs} | +| 分支数 | {branches} | +| 近 {active_days} 天内有 issue 更新 | {recently_active} | + +## 证据与复现命令 + +任何人可用以下命令独立复核上表数据: + +```bash +gitlink-cli issue +list --owner {owner} --repo {repo} --state open --format json +gitlink-cli issue +list --owner {owner} --repo {repo} --state closed --format json +gitlink-cli pr +list --owner {owner} --repo {repo} --format json +gitlink-cli branch +list --owner {owner} --repo {repo} --format json +``` + +## 方法说明 + +- 指标计算为确定性纯函数(同输入同输出),实现见 `scripts/research_tool_template.py` 的 `compute_metrics()` +- 活跃判定阈值:最近 {active_days} 天内存在 issue 更新 diff --git a/examples/research/scripts/__pycache__/research_tool_template.cpython-310.pyc b/examples/research/scripts/__pycache__/research_tool_template.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81c095510600e2ec91e571656c41d76bb14b2d05 GIT binary patch literal 4145 zcmZu!?Qa~#8Q=HY+dIGHg%AP_$BThedT}Ui1qdQ&5(I7H25^foh12C`eLm-I@0gt< zv2~VQj1!W^2`Px6IB9*zKtd%4)HDf>A^i*b2kghHeBz5MmHwVtzhSYboqgt+ znP+C6*WXoIS{wz>ciUd<|4+N3d`q3RkB-hWilX}e1i_RZg{e#{sb#H4Q;F6~dXFx@ zMvo!CW{)YqR*!|RQHqsgJ+ZRgW6SwwDPDGZ9MD!zg2j50uH&|hs1vOXrN6DejoEiA zJ?-oP*1}rfF?u$z2iZop3Dic`!R}()Ky6|Vv3uB0Ky79Zv+e9&P>C|I8pSV49;@jc39cTXd=i!Y{!!uXrZ@o9)I5jspJ^Rg{ zu+{9%_pvHEOY`c@`RgO$^rzvizs-$)M(dg%e6=t)OrseGkWa`WPmbgC0{xmpu$w(JUC%=?nplxL4Fhzo?1?~wEbSW`)>K_cgQ z6_FFgisyHwwZLXM;fi9}4dT=?w&nL}Q_sTd@ap&(WBQO8tRZ%*CrCoX7PsR@BI-e&P9V($XSVoTK#oVd09y( zy9Vt{rnkb&IT0BBe#PsC+FJ9uDlX<`D>VTN1cq>r3QqT1R8xcTRBx_Wa@muqK&u>n zZm=yCsljC_m-7mTI&x5RGI0Fbp(?NBUEdFMUh@Kd&;AzzCz_qFFgHlPRP)F%`=WLI zm7*6|zF?3Om{4)CD!VuaPhj`JELCz0!V0cP>zouE=ok!Q)!YCLT~_#ZI^|29eU-A? zSvv&rwN5gn-?_%xo$k>b%#D6D!*WXXVkU2UN>0s0y0Cor7JLtzF%_(%^l>ci?K{b z9&6FcWavzTVEBjp_C%WTil8zRLZ; zj>cR+9o;%@^80WZ{s1*Ur6xnoPHM;;QFAY-Ahs0W_oL(6Xzo$Bs+*xg-buqc&m9SL zuX2P_03n?7qWG4ZWw`-tUGzkr*vk*uAdXjRa~r%9T2${+hV;5Vp)d_TzCoyx)}yJ(szP3?&e)jlm@bqAEe{_ZpaGslIukdVk6F~GQZn_hd^a(k zq2@v2+Hg{u>8`za6cSkyzuxED*442c5aK_M##Zk^;;`ZWhPDxLpdpbMs-+tLOX`NI zeQ)bDL(?>jYyls?Ma{An`ETeLv8A&3gA7C2hV(6Cf=(O71k->alE@`gPmGI%A52MG zM5kk2(e<9ff?5ZfsFiuv5ltId;>b0@DghyMtqFn{Z8sUWJm zTePPXd~iu{^W4Nbsi&62VEqS|!V_cR&FT56sc`h&*&C;$BhC+x&NZgyrzRH~U(JnA zMJL7{(kf|Rx_`rraf!OrAu=9pF$K zQ~Nb2R;g3kL2+-Wf>N_Ff|nT=kP;@<(<&5hbuF4T(VKbvvKrW~hgw+l3Y_*_n@fXa z!9_J$jyNYepbv1fdV#bV*xmaM9NZ8058?<B{JKCT z6rjuy164POF}F8YD~UlflR=%VJ&(cWxoZpOu7-b|269imiYIg6KxXe=oMqwWM~H;* z>-P~Vso!<)xji%9{QISF^pkLWI-EKm4$lC{N5P1QrHtO^mZ}5^Bjh&-k0KZh^ov^m z2RfX+43v2ny8O4q1Eix_sm$(_tRLx3feox&&2iriTA$^GS{Zr#Aa!|=%rO*`B_4ka z>jOv5B&HuFWlCZES2SeRM4s>f!Gkp@gEC+g1mtspwFFe1BfN+|3=*!AcLrva12!N? zv|Lr<#YjE{ZRG#!@QG2*6+ODAzYR?i>A6*Hmn~AUM!kp7H&k9A>2@|t^RwBs5ecWP zzzLIZ%4n!~5*HEJiNKMhR>$qA4$<5-_oj*@_3{HrGH=P5>&8iDI{BkJt*((_F17k& ztF>q+vPJr?AePOtNUgp0mX@en7I z@J-YpLrN|6;{O5!#sIe~rMKL`g2CYt{AHSyqDIl#&4SOX~U=5U6M|YA=+}ZBfPQu;*JAVIvLL%gS literal 0 HcmV?d00001 diff --git a/examples/research/scripts/research_tool_template.py b/examples/research/scripts/research_tool_template.py new file mode 100644 index 0000000..5039d08 --- /dev/null +++ b/examples/research/scripts/research_tool_template.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""科研辅助工具模板:采集 → 指标 → 报告 三段式骨架(纯标准库,Python >= 3.9)。 + +自带可运行示例:仓库科研活跃度快照。 +改造为自己的工具时只需替换 collect() / compute_metrics() / 报告模板三处。 +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from datetime import datetime, timedelta +from pathlib import Path + +CLI = "gitlink-cli" +DATE_FMT = "%Y-%m-%d %H:%M" +TEMPLATE = Path(__file__).resolve().parent.parent / "report-template.md" + + +def run_cli(args: list[str]) -> dict: + out = subprocess.run([CLI, *args, "--format", "json"], capture_output=True, text=True) + if out.returncode != 0: + raise RuntimeError(f"{CLI} {' '.join(args)} failed: {out.stderr.strip() or out.stdout.strip()}") + payload = json.loads(out.stdout) + if not payload.get("ok", False): + raise RuntimeError(f"{CLI} {' '.join(args)} returned error") + return payload.get("data") or {} + + +# ---------- 1. 采集(按需替换) ---------- + +def collect(owner: str, repo: str) -> dict: + open_issues = run_cli(["issue", "+list", "--owner", owner, "--repo", repo, "--state", "open", "--limit", "50"]) + prs = run_cli(["pr", "+list", "--owner", owner, "--repo", repo, "--limit", "1"]) + branches = run_cli(["branch", "+list", "--owner", owner, "--repo", repo]) + return {"open_issues": open_issues, "prs": prs, "branches": branches} + + +# ---------- 2. 指标(确定性纯函数,单测覆盖) ---------- + +def compute_metrics(raw: dict, now: datetime, active_days: int) -> dict: + issues_data = raw["open_issues"] + open_count = issues_data.get("open_count") or len(issues_data.get("issues") or []) + closed_count = issues_data.get("closed_count") or 0 + total = open_count + closed_count + close_rate = f"{closed_count / total:.0%}" if total else "n/a" + + recently_active = False + for issue in issues_data.get("issues") or []: + raw_date = issue.get("updated_at") or issue.get("created_at") or "" + try: + if now - datetime.strptime(raw_date, DATE_FMT) <= timedelta(days=active_days): + recently_active = True + break + except ValueError: + continue + + prs = raw["prs"] + branches = raw["branches"] + branch_list = branches if isinstance(branches, list) else branches.get("branches") or [] + return { + "open_issues": open_count, + "closed_issues": closed_count, + "close_rate": close_rate, + "open_prs": prs.get("open_count") or prs.get("total_count") or 0, + "branches": len(branch_list), + "recently_active": "是" if recently_active else "否", + } + + +def conclude(metrics: dict, active_days: int) -> str: + if metrics["recently_active"] == "是": + return f"仓库处于活跃维护状态(近 {active_days} 天内有 issue 更新),适合作为科研协作/复现对象。" + return f"仓库近 {active_days} 天无 issue 更新,科研复用前建议先与维护者确认项目状态。" + + +# ---------- 3. 报告渲染 ---------- + +def render(owner: str, repo: str, metrics: dict, now: datetime, active_days: int) -> str: + return TEMPLATE.read_text(encoding="utf-8").format( + owner=owner, + repo=repo, + generated_at=now.strftime("%Y-%m-%d %H:%M"), + conclusion=conclude(metrics, active_days), + active_days=active_days, + **metrics, + ) + + +def main() -> int: + ap = argparse.ArgumentParser(description="科研仓库活跃度快照(科研辅助模板示例)") + ap.add_argument("--owner", required=True) + ap.add_argument("--repo", required=True) + ap.add_argument("--active-days", type=int, default=30) + ap.add_argument("--now", help="时间基准 YYYY-MM-DD(默认当前 UTC;注入固定值可复现)") + ap.add_argument("--output", default="report.md") + args = ap.parse_args() + + now = datetime.strptime(args.now, "%Y-%m-%d") if args.now else datetime.utcnow() + raw = collect(args.owner, args.repo) + metrics = compute_metrics(raw, now, args.active_days) + Path(args.output).write_text(render(args.owner, args.repo, metrics, now, args.active_days), encoding="utf-8") + print(f"report -> {args.output}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/research/tests/__pycache__/test_metrics.cpython-310.pyc b/examples/research/tests/__pycache__/test_metrics.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f916c8e577776fd73cc7d015eaf8579931b870d3 GIT binary patch literal 3064 zcma)8&u<$=6rR~x+Z)GD(ljk?f=Vfrb}31m0&OV>RVqKk0Rw6gk}s>x&NSI{cfHK4 zTM`)w$*qKtia2uUAwdW!5(<|}Ao`!|723vJBQBLHcyBhg6DOwFnzysFGjHDazBlhp zy<9F5_!(25EPZMb@&~@`{N>^05;XNQ2u?T+NG*HP8ijWwFhZkdP#j|c3(cBIi6Be4 z)jTJnAWUJk=<3iKSt>3Sxj98zbS;~m3x>l|trV7PWmsYGJTGvo#cCsbj1TeREm|9W zpYUN`xb^u1od;~^DwnxUrK|XrjkXHp=omeAa2fd=3eV`2%ld4Tqi@S+P z{7^vO{Mbzv(}MD(A1BH-(owG-#*IWcp-80fsnqi7kry<$@c$|h(uPLb=p!$O`nG@A zH=$jIrd|QjBr#hcP1-a#y~~;`F8ZWta?+r39G=Eqc7-f}yYOkV>N-pFzETaL(qcUp zk>k}HQIeLtpsobZ-fWf*>u=i=SuDZ2dFe*pqEPF^&PlXO4x=cc7(p?LVyq(`zc;&B z55;U_9`rY6rHJclcEL{qKe|5c1^%q~!VTjfDnLEH5+;O!DL^$rL@Fji{d8gASoHO3h?;c4<5&%-l^%)>LU zc_+0(#(K@lxULQHQE=K=I(`)_yPVPWVaAq6L4D_kdm=HKl+(~jNJ4Q>&h8kA!Oa%? zk~YaIYf_#&N_aktM9^KRwsCPAP5`luo=4lqU?44ORvZaYw8Ab_POh60x6!~;R{iL+ zbl6o&$mHG68*Z>}q^1(VwUmZE8sq^~G>WcudfE=dpu#Ykpc8cE#9nwaeGB%$?Z)5< z(1LJV1c1^VKn(!OxKDHCD7(w?jWU(zYx`>;0QFcbYPJ6FBxd2>IE6*;2)?D zVRnc1r34B=mg@#tzN#~+%`!w2U``tY#->?@)oe`vC$K5aLMLH6H>DfL5VC5~jfHlJ zCSqm~5&JLP0}>0A&UmjuCxP0>jm|mZxjR|L1E@`!FrII*Uat$akOiN70nunks8bH1 zK$y0vQ=P=KpJl$IIBLje;gM3=pE<@gZY~lbLqGCW;(JJd!b3AbkJFWtgCOr(Q?-k! z+qHN2yQ7DHzq{K)JE9>hnP)QSCa_yzIcNYtOJI zCChvmDO(6;lCB)tX=he%vPlDNmaoG~8!W@B@! z_y#w*2&+HLPbBKQ=Ibaez>TUN+z@F#b|n;^^l;~jx4#?YxLSbNOoU}ww7-L4sD6(;D{Fr5xsQJ&oxIx4V literal 0 HcmV?d00001 diff --git a/examples/research/tests/test_metrics.py b/examples/research/tests/test_metrics.py new file mode 100644 index 0000000..4a03b6a --- /dev/null +++ b/examples/research/tests/test_metrics.py @@ -0,0 +1,60 @@ +import sys +import unittest +from datetime import datetime +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) + +from research_tool_template import compute_metrics, conclude + +NOW = datetime(2026, 7, 10) + + +def raw(issues=None, open_count=0, closed_count=0, prs=None, branches=None): + return { + "open_issues": {"issues": issues or [], "open_count": open_count, "closed_count": closed_count}, + "prs": prs or {}, + "branches": branches if branches is not None else [], + } + + +class TestComputeMetrics(unittest.TestCase): + def test_close_rate(self): + m = compute_metrics(raw(open_count=2, closed_count=8), NOW, 30) + self.assertEqual(m["close_rate"], "80%") + + def test_close_rate_empty(self): + m = compute_metrics(raw(), NOW, 30) + self.assertEqual(m["close_rate"], "n/a") + + def test_recently_active(self): + m = compute_metrics(raw(issues=[{"updated_at": "2026-07-01 10:00"}], open_count=1), NOW, 30) + self.assertEqual(m["recently_active"], "是") + + def test_not_recently_active(self): + m = compute_metrics(raw(issues=[{"updated_at": "2026-01-01 10:00"}], open_count=1), NOW, 30) + self.assertEqual(m["recently_active"], "否") + + def test_bad_date_ignored(self): + m = compute_metrics(raw(issues=[{"updated_at": "n/a"}], open_count=1), NOW, 30) + self.assertEqual(m["recently_active"], "否") + + def test_branches_wrapped_shape(self): + m = compute_metrics(raw(branches={"branches": [1, 2, 3]}), NOW, 30) + self.assertEqual(m["branches"], 3) + + def test_deterministic(self): + r = raw(issues=[{"updated_at": "2026-07-01 10:00"}], open_count=3, closed_count=7) + self.assertEqual(compute_metrics(r, NOW, 30), compute_metrics(r, NOW, 30)) + + +class TestConclude(unittest.TestCase): + def test_active(self): + self.assertIn("活跃维护", conclude({"recently_active": "是"}, 30)) + + def test_inactive(self): + self.assertIn("建议先与维护者确认", conclude({"recently_active": "否"}, 30)) + + +if __name__ == "__main__": + unittest.main() -- 2.34.1