From d48e083959041d436bdb172c8201578e292396ae Mon Sep 17 00:00:00 2001 From: farmyobutu5233 Date: Sun, 5 Jul 2026 09:49:45 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(workflows):=20=E6=96=B0=E5=A2=9E=20doc?= =?UTF-8?q?-sync-automation=20=E4=B8=AD=E8=8B=B1=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E6=80=A7=E5=AE=88=E6=8A=A4=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81?= 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> --- .../workflows/doc-sync-automation/README.md | 48 ++++ .../doc-sync-automation/docs/verification.md | 38 +++ .../doc-sync-Taoyouce-gitlink-cli.md | 11 + .../doc_sync_workflow.cpython-312.pyc | Bin 0 -> 15533 bytes .../scripts/doc_sync_workflow.py | 258 ++++++++++++++++++ .../doc-sync-automation/tests/test_drift.py | 121 ++++++++ 6 files changed, 476 insertions(+) create mode 100644 examples/workflows/doc-sync-automation/README.md create mode 100644 examples/workflows/doc-sync-automation/docs/verification.md create mode 100644 examples/workflows/doc-sync-automation/examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md create mode 100644 examples/workflows/doc-sync-automation/scripts/__pycache__/doc_sync_workflow.cpython-312.pyc create mode 100644 examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py create mode 100644 examples/workflows/doc-sync-automation/tests/test_drift.py diff --git a/examples/workflows/doc-sync-automation/README.md b/examples/workflows/doc-sync-automation/README.md new file mode 100644 index 0000000..9382ac9 --- /dev/null +++ b/examples/workflows/doc-sync-automation/README.md @@ -0,0 +1,48 @@ +# 中英文档一致性守护工作流(doc-sync-automation) + +把 [`gitlink-doc-sync` Skill](../../../skills/gitlink-doc-sync/SKILL.md)(中英文档一致性守护)包成**可直接运行的端到端工作流**: + +> **采集 → 检测 → 报告 → 回写**:用 `repo +tree` 按命名约定自动发现双语文档对,用 `file +view --raw` 拉取两版内容,做**确定性结构比对**(章节大纲 / 代码块 / 表格行数 / 版本号),输出分级(🔴严重 / 🟡中等 / 🟢轻微)漂移报告,并(仅在 `--apply` 时)用 `issue +create` 把报告作为 tracking issue 真实回写到 GitLink。 + +与仓库内已有能力的关系:`file` 命令组(文件读写)→ `gitlink-doc-sync` Skill(AI 语义比对与翻译同步知识)→ **本工作流(确定性可复现闭环)**,三层互为支撑而非重复:Skill 负责需要语义理解的翻译同步,本工作流负责可进 CI 的确定性漂移检测。 + +## 交付物 + +- `scripts/doc_sync_workflow.py`:文档对发现 + 漂移检测 + 报告 + tracking issue 回写(纯标准库,Python ≥3.9,零第三方依赖) +- `tests/test_drift.py`:确定性回归护栏(同输入 → 同发现 → 同退出语义) +- `docs/verification.md`:真实平台验证证据 +- `examples/demo-outputs/`:对生产环境真实仓库运行的漂移报告 + +## 快速运行(默认 dry-run,不写远端) + +```bash +npm install -g @gitlink-ai/cli +gitlink-cli auth login + +python3 scripts/doc_sync_workflow.py --owner --repo --output-dir outputs +``` + +- 自动发现失败时手动指定文档对:`--pair README.md:README.zh-CN.md`(可多次) +- 指定分支:`--ref develop` +- 真实回写 tracking issue:加 `--apply`(请先在自有仓库演练) +- 退出码:`0` 无严重漂移,`2` 存在严重漂移(可直接作为 CI 门禁) + +## 已在真实平台验证 + +全部证据见 [`docs/verification.md`](docs/verification.md),要点: + +| 验证 | 对象 | 结果 | +|------|------|------| +| 文档对自动发现 | 生产 gitlink.org.cn 真实仓库 | ✅ 自动识别 README.md ⇄ README.zh-CN.md | +| 漂移检测 | 本仓库 README 双语版本 | ✅ 检出真实漂移:代码块 32 vs 29、表格 43 vs 40 行 | +| `--apply` 真实回写 | 自有 fork | ✅ tracking issue 创建成功(issue #1,API 回执确认) | +| 单测 | `tests/test_drift.py` | ✅ 9/9 全绿 | + +## 设计要点 + +- **确定性**:漂移检测只做结构比对(标题大纲、代码块数/行数、表格行数、版本号),同输入必同输出,可进 CI;需要语义理解的翻译同步交给 `gitlink-doc-sync` Skill。 +- **围栏内解析**:代码块内的 `#` 不会被误判为标题;表格分隔行不计入行数。 +- **安全默认**:dry-run 为默认行为,`--apply` 才写远端,且只创建 tracking issue(可追溯、可关闭),不直接改文档。 +- **CLI 为唯一依赖**:所有平台交互都通过 `gitlink-cli`(`repo +tree` / `file +view` / `issue +create`),无直接 HTTP 调用。 + +> 依赖 `file` 快捷命令组(PR #330);在其合并前可用 `--cli` 指向包含该命令的本地构建。 diff --git a/examples/workflows/doc-sync-automation/docs/verification.md b/examples/workflows/doc-sync-automation/docs/verification.md new file mode 100644 index 0000000..7a1a413 --- /dev/null +++ b/examples/workflows/doc-sync-automation/docs/verification.md @@ -0,0 +1,38 @@ +# 真实平台验证记录 + +验证日期:2026-07-05;平台:生产环境 gitlink.org.cn;CLI:包含 `file` 命令组(PR #330)的本地构建。 + +## 1. 文档对自动发现 + 漂移检测(只读,dry-run) + +```bash +python3 scripts/doc_sync_workflow.py --owner Taoyouce --repo gitlink-cli --output-dir outputs +``` + +结果(完整报告见 [`../examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md`](../examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md)): + +- `repo +tree` 拉取根目录后按命名约定**自动识别** `README.md ⇄ README.zh-CN.md` +- `file +view --raw` 拉取两版全文后检出**真实存在的漂移**: + - 🟡 代码块数不一致:英文 32 个 vs 中文 29 个 + - 🟡 表格行数不一致:英文 43 行 vs 中文 40 行(功能表滞后) +- 退出码 `0`(无严重漂移) + +该漂移与本仓库实际情况一致:英文 README 的部分示例段落未同步到中文版。 + +## 2. `--apply` 真实回写 tracking issue + +```bash +python3 scripts/doc_sync_workflow.py --owner Taoyouce --repo gitlink-cli --apply +``` + +- `issue +create` 成功在 fork 仓库创建 tracking issue: + `[doc-sync] 中英文档漂移报告`(issue #1) +- 通过 `issue +list` API 回执确认:`project_issues_index=1`,正文为完整漂移报告 + +## 3. 确定性回归护栏 + +```bash +python3 tests/test_drift.py +# Ran 9 tests ... OK +``` + +覆盖:结构解析(标题/代码块/表格/版本号)、代码围栏内标题不误判、四类漂移全部检出、相同文档零发现、同输入同输出确定性、文档对发现约定、报告渲染。 diff --git a/examples/workflows/doc-sync-automation/examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md b/examples/workflows/doc-sync-automation/examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md new file mode 100644 index 0000000..df27654 --- /dev/null +++ b/examples/workflows/doc-sync-automation/examples/demo-outputs/doc-sync-Taoyouce-gitlink-cli.md @@ -0,0 +1,11 @@ +# 文档一致性报告:Taoyouce/gitlink-cli(ref: 默认分支) + +## README.md ⇄ README.zh-CN.md + +| 等级 | 类型 | 位置 | 说明 | +|------|------|------|------| +| 🟡 中等 | 代码块数不一致 | 全文代码示例 | README.md 有 32 个代码块,README.zh-CN.md 有 29 个 | +| 🟡 中等 | 表格行数不一致 | 全文表格 | README.md 共 43 行表格,README.zh-CN.md 共 40 行,疑似功能表滞后 | + +--- +*由 doc-sync-automation 工作流生成(确定性结构比对,同输入同输出)。* \ No newline at end of file diff --git a/examples/workflows/doc-sync-automation/scripts/__pycache__/doc_sync_workflow.cpython-312.pyc b/examples/workflows/doc-sync-automation/scripts/__pycache__/doc_sync_workflow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cae7398c13c740eb44c043e783e8d79413cc3432 GIT binary patch literal 15533 zcmbt*X>=1;x?q)5vbK_BdBF?V$SeUFFA&V;u*AWv0kathZa|S$vMu8+RVBcBa-$Fk zGB|CM&_N_5n1oD3L%?*?!2!}C=e;vcX6BqOVe_n1nDg?wE!+HRzmRukl3(-PTPjHy zk>vHS0+v|TC3_1cr>g@H670(dF-%v&ls%YX7OA^Fsf+1=MhGY~?WDSX* ziW&ueDr=PZN!3vJsj5-IQ`wZ#tgcZvYicwk>PIzcn{_ohNUQ1zCglUNPQj=d4Wq4B zKuX8x>!GAxS)+%xG-F_lXO%THV`5SnGfo?vY6Y=|VA75dO!_5-G`CC9Db^UHwHZ*G z`9!UWDPSfYA!}0MnFUWXJhS1M#^f9!YSI;iQ`tCMe(m^^$%XR5cwSD)@M|V9lc9eW zGX>&oCJ*8qfSeDIbD61-o9vvzSoQ&AHXFuJHI~Z|;!h}_EWgDiSZ5SSqFvFhuugO5 z!D|lpe=>IB+Q{3-M$UgY-0vGZc74S6apa1B5sgAG;-xL-LoapfdSI!`74nt@4zdJ-g;}~{Uak+ zPeC^rdh}B7*zv=W3m=c({Jo{rGJNa9(cV`h@12J9*!kc{@9nYkT_eBkgDk)uIeR(M zeFNsHn-8GzivZ}cKfM(>b~ECCee~wX0CBkgqld@dD24SK{iL&W?DJcZKV5+?8|~~q zru9IJW#snZ(T{IQy>(?J7I_ukDK56RwKcU{Moxb^a{Rc|0M>c9|E7g!?T&q}mU@ee z~-iIZEscdfN8(Lc|%a>W^ zmn^}{+m*PK06B8%Dr|1#!rKpD3l9J3y|F*OjkfOLt-#oo3l@fLFJ|4apIyWKZ@^BC zeQ|d5(pB659hWMrLew&L-tK6!bDXHEb2*zBD=AW&?RqeDZ(aAo_Ud(2Yf$6N#|mN)(-MnixGUFIRIORH zVH56{7SGAO9-@=~5M64k@;_7HA#GkXi8c@?rA>5G&|v)Bsq4|ix$|=30|(T^f-;(V zv=9=f2`2$~i>Og-huiK1OtlnBaY_+ODPt)LrP@{26j8m})xyA*iyF?k-^sf8c2VQ7 z^UnHK7E(>EkcPV^rZ7(4?rN&Cszi;=)?#mV+H9iEW@~O`+)XG=+ib76?M=}dCC9U( z$!4>+w6yXV0%x;bBp#tD|5#hv(Aw-Qb=SIEcy}r5Y-{C86V6Mi^Ps)Ct;xxi9%yCv z)it#q;7XHR|5DDuy4rYLXG5sLCO0o>YiDT~2uB5w;3h$Il=#+Yer;no9?_;FFS1#3 z!{lq*2Gxmci*Tuvx=hNrtVZReoGA=mV@6q{c4{DpR~K?xoKvB-hMW%PQc&&*D0&P< zjdw+3)rxdwt799_x*fcmb;4OUG0r->yNS2e*&RIInFgmFZ5zj;Qzhyht&G!F3){7i z6LmNPn9K=j-d+p)!@?qp8rWYBHj}He(ySS}v1uquM-e)6BGu%AeVc;vnJCIbg-TZo z&*r1-R1{fIgw|79xoniml_M)8xaANXCH_4#H&`~9IoG$=&kRs=!&%wB7kknNsO<2h zEZ_Pb6UB9r{Pvm|*i!azYS|iLFpe2wt+D^b#K+ zQVvA@vhosoLkJxTuc9%UmP(|Uq4q6;SE=UFC^#@GCZ$m;=i*N1fvFMn8k?K zqg12e@jS}OWbh!#<&p&SGd|hor~181yhq6+A10Ic-$qz9?qYyD*7w@zso>)j?XyqE zzBmPBz{2rNtDCnVvUc_|;MwZcR;6fk*xS%)vq4pxn-?kGd5{-Xjs~Y=A3kIrI^mL9 zMAG6$i(`pgxHfkEqjHN?BkDMJZ5!L_Z~~fE0!9~gtP?=Cpz|owTj9F9nw_gywv`pB z##R?#6_|;WWknSnJ69Xd!ssv=%FfmU3?Rf}3hU(H1a(_lC8!pZ4>9y0^NI>RBm&n-T)xVQ4%Q(KhsxH3%GL~&t-DuNdw0iQ_I$NxXyePFjV}*u ztQF=v1e$rIRG4(*ghHzWsB~KQvd(1!m5mRsLuM>tMg&?Sd}b>l0Zb;GMMmi*7|KgB zDzAb`sn>R>yh>K>Rl!+O)+;=4aynG;v+Y%RsmmIfC4>FdR*6)dtI6q6&DrmA9{5pR zTnyyi$^Mv9T#Rt32cB(+GO8eNKky@ZIO{Hw>^ztOJkW{(-dEvHB89q$6zTw5=B&f5 z*tyV#Lv#uj9D=mwx;iKCXt1HN*%eTS?m4#@A|DZ^(|Xnfb_G?zgZ&x(2k&OwJs{|J z`YOLx>%tnkCo3?$#~9M&`>6-|w4SoSwn2S?Kov- z1*{c??p<`PsD!nmyvq1m!73tHI3&CAUX^^CI#RqTjj?+#_W`7>^`^)x-=XGXxWKFC zV|QMzXET3hG<9sW#~~p~g+mNdnpd54Haf3bZqq_~)g798qC=Z7D-9pRC~`Y?xmQz3 z>{$Y5N|(@5$3HPDM|Nd+bxFt7%QJ^I&v>Na~j0?DpKmFTL9Dl{l3l1@$!S$qwoNV*JWL@I%-w(_>o-3i_?dZiLR_4;Xe(J1o6_J*M z@S54TLAs1EvAOmWV)M3>2V`s!kCEjOs?F|V;Y8NjIiP4DF12t?5_>C>{1X_{$hbI1D{x&l+>Lz+ zVC{rIw;duNRz`Er%oB~#efqcjIU!B1pYk__^`@Q!A$@+B&iKZh=dTExQhU}M_sYA( z^i~9R4W`ceIyLh@)I?hTxSE(+G;WOVmS^$q+Sx10>s!4oKzJdwyQGn`uJE>DG0nT* zx|>@udp9#@cL_we8lKh_*4?#^#C?t-<8NW#68BkVWIBMg>QFF>4j^caF;ok$77cQ{ zN}`9{^CIjr<)xDCt=tZsf}{vYDepV`ETI1)FL{t`M6;U^7zz-{g0=tb&O20OQ8kA=@SAIZ!za*Rz?$F z87>p=1>)%^gxIddRvFe0$siL#BN9I*1V(#=;4N}t<8(O{e|lppSQ%r$<+UQ=Ty|9f z1|b21=-g|e7ic^{yCY!-NrMB57f^US{2(CdZvN(bHKx0PbWC>x>6qdMaLQ_lV^+TW z^2;p3mmc%p+0*}NUeUr1xO{VFmlRrOu!!LhNw3GacenRfySZ|2@t!%my)z!Wg9w8n zhV79Bq8AAg?xC010}wk%FZnikngl$kp@??k6XgN&B3XqcGtiePO6H2>ehzh)**ERd zdS{FCU>m#KGp)_ea!wmC-I9P=vJ}brP2BR5Serx8lS3zVjQGDkLdaPjnz^i(JX-*d zA2@Ws4%0@`nubL`;QQN}ueiRj~Q&a)HcQi;cCc?0y zvdP&Zs_NNRcbljIS}k>oiJD>xNFh)x>UON!x^3O2D%;joqPh-r7WSqltD2pOV33@s zc7Z$q*iIS=1lblRXf%*i(dJ}CvYA7xEfMf2J8PP-2y82~Xn;RA3$P$yXHCWlqpu=t zOa}yQPV=n~r)Hcsoiq)l7Wk=fde-UkljUzM^=rcV^dWsdpzDBpP(Sl))1+{!`Ly9p zL$5iMI_J9paS5(u_3I{cI!HILzpGlafjXy)s z2H)BzS$K42Fn4eqaO-IT_yAzp8=-Jws3f{o4n;lS>pgJu5$uLB#-62Iz+d6bUJD_WYNQEK?n!R^^Co$Y067T_hIuC` z^#F$;zj1sQ7*lc>OsWUas=W65(C#n(1@CMIt4~^Ti10q?$!7*N8Skl*?g~=fO$Y!Z zNh#iyWxrsCnam`-D_JnhByeCZ*?3p7l8*!RImvb<&g1Yg4hKd+YMco{-UIoWw5Rfy z^z6`g=)5|w9>4o8uMThx9mO>u#COnM+NvnD}$7Ex*I8VV&s}9=&}NREp9dEK<3+5tAHax*i)BO($|Rh^0JaP#e8)bNEju+~|&5hWmdP>x4!{ zObaS}h$i(AX$tV$NNptRo3toy#E2#qTRfCyKWD+s0aE0~mB{VGW1k*HCc_0Y@$_PR z;A7K6xJ{a#R4=VZ$ZmCS2nSFeu*euH0n_|{QbReS6v80^X|ZbYC*PWR$n`TOm6QHv+o zPPC_wW9AVx2*zRZC&*)+or9RtG2ReIl+WM)^Uiw zM!7G|(k6RzEn{Eqnc-+{239PA9pw`E(P*nH(33-ee8dG~@Rh5ci=KOya?E`IXyra< zyVWQ$l?*h(vOBi^h{|UBL5N%}QDcU^mWvxRK;hiV@=k``f;w3s5-C(-HHlgz;e*%? zYEc~)^KqI*g1e}~1)>IVp~wUyk`1D&*2cHCK^1;b&8>*nt#cl5f^xgo1}Xql1rK&F zf{@bM($sFNjXrq}k*z3mDnBGOQ`(b@!ybpmufd=DSJ(<4@vX`1TMNpXoV;^}cMJpc z{4<80c|mh<-o=bQ^QD~QTLK2(DuJFK&dxh$e8&iUytSZMDhy33zUvWw#Ryf-PCe)< zoMBCBx2e-~Rt-F-FsV=|0&PXQzpiK1+39B;y))l!ygWVVyi_=lyFkcU*jL~0?B983 z?f=>~xTRXyUNf-eSA%sDRXq+6H2XqoF+8% zkUA@*&I&9Tnp_l`Ty#%e^!V7tLg~t|DcipH6 z?)Nm2k$ZM}fH~KErg3Qyq3j2dF8krT zr9|4oA4Ky4t{BeuNyGYi)Hw>GKN!|8q`o96h`&;6*FQ&nWu{O(S^FYEeKnJU*n!Zy z9ME;bGRSqfA;{>0DX{|6UZqzNrM(o;W7T_VFe@lJ5{w9p`Uvp1PihOWgrdRTE@q(5 z2>yh?XcK5NK-e=1I!|Z^@(U>IpVCL?O@Vpnz3N2s9-{3@M~F!x&_csae@ZJlVIDsV zr>puYPz*l{#hB0nnHhipV3{Y`+L-v*4uJ^)=|m<3L_}aEQj^z0jTveX>o6fiYY=su z5TZ57C5*K9VrC7{q3i`M(9f^b!>6`NPgsfMa5Ca8n9RnQoD!dMTvG+ws7Y0Bq$-<^ zZL|rFQ88yAku1f06nNi&)ecBHdP~Wl20VmDPF=+`9(-{`;xWpOHeo*eAC*HCQz!wg z#|M?{0EKRXLO-T(kE)t7WmS_z>q(XYC{`uA2Oc76W8Lt4%uvhPpu}TgL4yNYaAa#_ z`2^}NYoxBDeu(3hv`wax^Yv#I1?C0pz2;NOo{G~c=j)?dr<}G@Y?1%-kix|}D-dGzqQY?8b z;F$y5tOXL+ZbjZd0g2(y-x&S;3M3>B%i{Iqd5dvK{_rTg7Iqc@^C+Q{M*$s_(A^Zz+p-Ae$BCq7! z&6pb!4Kc249e6Ndri?}ALKYoG7IDmM5kywq1XCH9+9mrt7~QZ(iHZl0o+h>f_4qZ4 zd?>;@kM5w%zsPDw!()5Tz70)&4}Y!-E7`&(Yj9>TtB>rP(Ko5@;2q_?=eOVawXkA` zuw$2Cs1bJW6=)mSuxL{V9f|={$pBp<5i;_yC#*4a)19=CSv9EH^v#@SLz-EBZBKdN zV6>u#3oHrD>D?YI4;BfTiv~4|<%+F?n(VJlSz+4Ty`gi1kTYkHE{bLXX@m3>q(PH` zFCEk@i)G3NH4DBm=YUa0>Kb_VY;&;U>iUc8L-~t@yv6-wf58`ppB45`xwGxA`7gO& z<=&-*>Yc(%yM|t>4ZTz=>~si@I-$N{sJ=B+-zwC#32m&9!`-L(aV3$K`>ix;&(`~z z?C~t3X#RLMq0$}O@al%1%t3IOp|mJ1m|F*_Hn0#;vwc86S)e9Asf$DVi|HUT zttMJ$+`_>uK`@7uM$<*AJC(o<6xcO8x}X>dbGvG;Plbx1V^Rm zFvzdu)4T^nt+-=nRNBOzj2=UAG2@h+x4;>Rt$=RWs*7#EEW%4xRs0Yl{||&UB$F*z zkq8b;mFS#FJaVQqDI0L**m=>0C(FJAP4A-S2%?X8pfiUxrkw%UXE{p9%~4BNGgL*&yqdp4!rge3UPn zFGXqkQ`@C6>Azsy3?_3zE2J2h5MuT~uyG`#`0V`T<)FP$M72ENxdwzRrVRys6A~UL z1ZEPGC9jlBok1Hk6|_Oh4n@n$3EIS@7Co3n5A6hZcKw!Hqpfg%P7 zhd3tymQ^J;g^%fim>eFd32AHH6_;`vd506qV*Gk+7j$6rM8tYR zK(M^Jx|kgrI7aBgTjgf|rL|0cK4Mzl4n0370WKoS6N3CENRQcAI-wG>U{jzjPmZ4i zFU?FzK11M;^PnclDTbQ-_f#IZK^+FKp%M9xO$c5+Gxf41K1%$QdJ{P7cqvohHGqtx z_~j>`m&k3BuTJun@#>-cC+{*z zB)s*EU;e&cBdcI$ZX=jyVtQQ#Xl`d!xo5yc6X~hM-#+=F1n9!_iL`Yg^s9zK4eNZx z?P5XGg_cF~w0Z+lP^FG~7E5k}E*5i(ZbXzvI!{E72S>V&p${7H_Zj$8hA#M6A#%Ih zf)r%nCtr@0VUiOJWP#^~uPh=L-Wj=k9vo{@z+D*;`S$^yNHsW{+C1Q|h`&ETMq<#C z0oist-RseK@G(NdHw(!>Nszu`NcbcHc;ktm5p-G=>^T6)_Cn;L!9Nmvv=%ciR#Z!A z4tYm{|LBkU1fN!sEXT%r%!s%V&jjK_((gcEP=;Wod=P}SnEEIR_2k4MY7nVMBf^y+F;j|)3(g>|428KWy4X}iyGqOKHHuzOd2SHDP zd+E~^5Bq$P9dn6-SFK6cvB;H4D62>=c5z+9+4Xwc(SqJ*bf3?JI5kz8+Pf% zx@GJLfzbe#)>uT#MV)kSZ0HWDn)jiH?f`r(0{0gjG}x0Eq;#*9_Lg?ceS;c31<#Si z4#gtb##W<|HlpY)6vc$dsJ(G7k ztiBw-DwO)%aB4yCjE@V?7Y3QDu8Xezj4!f3%f7=S&yT^>&3@H`RPa|BNS*0deQg52 z6SI)EVQ|}yp>2CY+xFb2_lDCmzBQ)$)_ha&Oh}W5gz5(w`2qf%=Zq&cU~8)yt?ksolm-V?f(m9y};yExxZ= zGHxKI&H6r-03CbTx$-mRz5K`R=iB>c+?aiBcK@0IL#1yu_|5i|onCQr#ei`-IO|Y) zq-74w2xcHPb4fTYC*TTFS9KS4z23fgebqO1UE9_7Vm~KLSs6-O>3cCunTDv`5S1HP z7+fPzxr0>M14`?E=Ez2P2(+bla<4(qm0aI;qvl%8ed_sPLuv@&tbx=m1BNYddSGKb zhdrkpp&Y;f133!@CoOoO$pdYC-E2lLkD1{Iwi7WFd4Wecl7Kh;L|e)#4k?Zbjuhirz#K(jQ76@*u8D!UmgA z3S$d3vO|I6zeuiQ8&GL0ijJe`Z4@D48{abgU6DuuvEwIvB_wK=N*{|YXTO5hB>XuF z;&CNOlK(;JNa|5GL8g8~=)WPf-w@_+2;ILCv;Q}dJ+2{A%=gl^3)GIVYLbvMS9o@{ zkPREPHmpiVHmA%v0#!7wrpyYT0i^c4{Ghqd9x^ZX8N-tcf;;;6hf-HVO$wpV{~rZK zrj8R3JyJq;9A}NVLPgFSCm@Pepj4`s%z2bek<%U-O{Dp|8G6z@K7}x5`?q%2bk+#j z+l1{qhqmtuZQnIusPXB(OUWh;-z_X6GrrqPrjt`16{$(ncNs-w{`g*!plHA6o-!v) a(Y}?(*1o#7v--r2`&0%X1u7RS8U7z}@|qt2 literal 0 HcmV?d00001 diff --git a/examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py b/examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py new file mode 100644 index 0000000..2d6e163 --- /dev/null +++ b/examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""中英文档一致性守护工作流(doc-sync-automation)。 + +采集 → 检测 → 报告 → 回写(可选): +1. 采集:通过 gitlink-cli 发现双语文档对并拉取两版内容 +2. 检测:确定性结构比对(章节大纲 / 代码块 / 表格行数 / 版本号) +3. 报告:输出分级(严重/中等/轻微)Markdown 漂移报告 +4. 回写:--apply 时把报告作为 tracking issue 提交到 GitLink + +纯标准库实现(Python >= 3.9),gitlink-cli 为唯一外部依赖。 +默认 dry-run,不写远端。 +""" + +import argparse +import json +import re +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path + +# 文档对命名约定:主文档 -> 可能的翻译文档 +PAIR_PATTERNS = [ + ("README.md", ["README.zh-CN.md", "README_zh.md", "README.zh.md", "README-zh.md"]), + ("CONTRIBUTING.md", ["CONTRIBUTING.zh-CN.md", "CONTRIBUTING_zh.md"]), + ("CHANGELOG.md", ["CHANGELOG.zh-CN.md"]), +] + +SEVERITY_ORDER = {"严重": 0, "中等": 1, "轻微": 2} +SEVERITY_ICON = {"严重": "🔴", "中等": "🟡", "轻微": "🟢"} + + +@dataclass +class Finding: + severity: str # 严重 | 中等 | 轻微 + category: str + location: str + detail: str + + +@dataclass +class DocStructure: + headings: list = field(default_factory=list) # [(level, text)] + code_blocks: int = 0 + code_lines: int = 0 + table_rows: int = 0 + versions: list = field(default_factory=list) # 形如 Go 1.26 / v0.2.0 的版本号 + + +def run_cli(args, cli="gitlink-cli"): + """调用 gitlink-cli 并返回 stdout 文本。""" + 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 fetch_file(owner, repo, path, ref, cli="gitlink-cli"): + args = ["file", "+view", "--owner", owner, "--repo", repo, "--path", path, "--raw"] + if ref: + args += ["--ref", ref] + return run_cli(args, cli=cli) + + +def list_root_entries(owner, repo, ref, cli="gitlink-cli"): + args = ["repo", "+tree", "--owner", owner, "--repo", repo, "--format", "json"] + if ref: + args += ["--ref", ref] + out = run_cli(args, cli=cli) + payload = json.loads(out) + data = payload.get("data", payload) + if isinstance(data, str): + data = json.loads(data) + entries = data.get("entries", data) if isinstance(data, dict) else data + names = [] + if isinstance(entries, list): + for e in entries: + if isinstance(e, dict) and e.get("name"): + names.append(e["name"]) + return names + + +def discover_pairs(names): + """按命名约定从文件名列表中发现文档对。""" + nameset = set(names) + pairs = [] + for base, translations in PAIR_PATTERNS: + if base not in nameset: + continue + for t in translations: + if t in nameset: + pairs.append((base, t)) + break + return pairs + + +VERSION_RE = re.compile(r"\b(?:go|node(?:\.js)?|python|v)\s?(\d+\.\d+(?:\.\d+)?)\b", re.I) + + +def parse_structure(text): + """提取文档结构:标题大纲、代码块、表格行、版本号。""" + s = DocStructure() + in_code = False + code_lines = 0 + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("```"): + if in_code: + s.code_blocks += 1 + s.code_lines += code_lines + code_lines = 0 + in_code = not in_code + continue + if in_code: + code_lines += 1 + continue + m = re.match(r"^(#{1,6})\s+(.*)$", stripped) + if m: + s.headings.append((len(m.group(1)), m.group(2).strip())) + continue + if stripped.startswith("|") and stripped.endswith("|") and not re.match(r"^\|[\s:|-]+\|$", stripped): + s.table_rows += 1 + s.versions.extend(v for v in VERSION_RE.findall(line)) + return s + + +def compare_structures(base_path, trans_path, base, trans): + """确定性漂移检测,返回 Finding 列表。""" + findings = [] + + # 1. 章节数量漂移(结构不对齐 = 严重信号) + b_top = [h for h in base.headings if h[0] <= 2] + t_top = [h for h in trans.headings if h[0] <= 2] + if len(b_top) != len(t_top): + more, fewer = (base_path, trans_path) if len(b_top) > len(t_top) else (trans_path, base_path) + findings.append(Finding( + "严重", "章节数不一致", "一级/二级标题", + f"{more} 有 {max(len(b_top), len(t_top))} 节,{fewer} 只有 {min(len(b_top), len(t_top))} 节,疑似缺失章节", + )) + + # 2. 代码块漂移(示例不同步 = 中等) + if base.code_blocks != trans.code_blocks: + findings.append(Finding( + "中等", "代码块数不一致", "全文代码示例", + f"{base_path} 有 {base.code_blocks} 个代码块,{trans_path} 有 {trans.code_blocks} 个", + )) + elif abs(base.code_lines - trans.code_lines) > max(5, base.code_lines // 20): + findings.append(Finding( + "中等", "代码行数漂移", "全文代码示例", + f"代码行数 {base.code_lines} vs {trans.code_lines},差异超过 5%", + )) + + # 3. 表格行数漂移(功能表滞后 = 中等) + if base.table_rows != trans.table_rows: + findings.append(Finding( + "中等", "表格行数不一致", "全文表格", + f"{base_path} 共 {base.table_rows} 行表格,{trans_path} 共 {trans.table_rows} 行,疑似功能表滞后", + )) + + # 4. 版本号漂移(轻微) + b_ver, t_ver = sorted(set(base.versions)), sorted(set(trans.versions)) + if b_ver != t_ver: + only_b = [v for v in b_ver if v not in t_ver] + only_t = [v for v in t_ver if v not in b_ver] + findings.append(Finding( + "轻微", "版本号不一致", "安装/依赖说明", + f"仅 {base_path} 出现: {only_b or '无'};仅 {trans_path} 出现: {only_t or '无'}", + )) + + findings.sort(key=lambda f: SEVERITY_ORDER[f.severity]) + return findings + + +def render_report(owner, repo, ref, results): + lines = [f"# 文档一致性报告:{owner}/{repo}(ref: {ref or '默认分支'})", ""] + total = sum(len(f) for _, _, f in results) + if total == 0: + lines.append("✅ 所有文档对结构一致,未检测到漂移。") + for base_path, trans_path, findings in results: + lines.append(f"## {base_path} ⇄ {trans_path}") + lines.append("") + if not findings: + lines.append("✅ 无漂移。") + lines.append("") + continue + lines.append("| 等级 | 类型 | 位置 | 说明 |") + lines.append("|------|------|------|------|") + for f in findings: + lines.append(f"| {SEVERITY_ICON[f.severity]} {f.severity} | {f.category} | {f.location} | {f.detail} |") + lines.append("") + lines.append("---") + lines.append("*由 doc-sync-automation 工作流生成(确定性结构比对,同输入同输出)。*") + return "\n".join(lines) + + +def create_tracking_issue(owner, repo, report, cli="gitlink-cli"): + out = run_cli([ + "issue", "+create", "--owner", owner, "--repo", repo, + "--title", "[doc-sync] 中英文档漂移报告", + "--body", report, + "--format", "json", + ], cli=cli) + return out + + +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("--pair", action="append", default=[], + help="手动指定文档对,格式 base.md:translation.md,可多次") + parser.add_argument("--apply", action="store_true", + help="把漂移报告作为 tracking issue 回写到 GitLink(默认 dry-run)") + parser.add_argument("--output-dir", default="outputs") + parser.add_argument("--cli", default="gitlink-cli") + args = parser.parse_args() + + if args.pair: + pairs = [tuple(p.split(":", 1)) for p in args.pair] + else: + names = list_root_entries(args.owner, args.repo, args.ref, cli=args.cli) + pairs = discover_pairs(names) + if not pairs: + print("未发现双语文档对(可用 --pair 手动指定)", file=sys.stderr) + return 1 + + results = [] + for base_path, trans_path in pairs: + base_text = fetch_file(args.owner, args.repo, base_path, args.ref, cli=args.cli) + trans_text = fetch_file(args.owner, args.repo, trans_path, args.ref, cli=args.cli) + findings = compare_structures( + base_path, trans_path, parse_structure(base_text), parse_structure(trans_text) + ) + results.append((base_path, trans_path, findings)) + + report = render_report(args.owner, args.repo, args.ref, results) + out_dir = Path(args.output_dir) + out_dir.mkdir(parents=True, exist_ok=True) + report_path = out_dir / f"doc-sync-{args.owner}-{args.repo}.md" + report_path.write_text(report, encoding="utf-8") + print(report) + print(f"\n报告已保存:{report_path}", file=sys.stderr) + + severe = sum(1 for _, _, fs in results for f in fs if f.severity == "严重") + if args.apply and any(fs for _, _, fs in results): + create_tracking_issue(args.owner, args.repo, report, cli=args.cli) + print("已创建 tracking issue。", file=sys.stderr) + + return 2 if severe else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/workflows/doc-sync-automation/tests/test_drift.py b/examples/workflows/doc-sync-automation/tests/test_drift.py new file mode 100644 index 0000000..fbf651d --- /dev/null +++ b/examples/workflows/doc-sync-automation/tests/test_drift.py @@ -0,0 +1,121 @@ +"""确定性回归护栏:同输入 → 同发现 → 同退出语义。""" + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) + +from doc_sync_workflow import ( # noqa: E402 + compare_structures, + discover_pairs, + parse_structure, + render_report, +) + +EN = """# Title + +## Features + +| a | b | +|---|---| +| 1 | 2 | +| 3 | 4 | + +## Install + +Requires Go 1.26+. + +```bash +make install +``` + +## Usage + +```bash +run --help +``` +""" + +ZH = """# 标题 + +## 功能 + +| a | b | +|---|---| +| 1 | 2 | + +## 安装 + +需要 Go 1.25+。 + +```bash +make install +``` +""" + + +class ParseStructureTest(unittest.TestCase): + def test_parse(self): + s = parse_structure(EN) + self.assertEqual([h[1] for h in s.headings], ["Title", "Features", "Install", "Usage"]) + self.assertEqual(s.code_blocks, 2) + self.assertEqual(s.table_rows, 3) # 表头 1 行 + 数据 2 行 + self.assertIn("1.26", s.versions) + + def test_code_fence_content_not_parsed_as_heading(self): + text = "```bash\n# not a heading\n```\n## Real\n" + s = parse_structure(text) + self.assertEqual([h[1] for h in s.headings], ["Real"]) + + +class CompareTest(unittest.TestCase): + def test_drift_detected(self): + findings = compare_structures( + "README.md", "README.zh-CN.md", parse_structure(EN), parse_structure(ZH) + ) + categories = [f.category for f in findings] + self.assertIn("章节数不一致", categories) # 缺 Usage 节 → 严重 + self.assertIn("代码块数不一致", categories) # 2 vs 1 → 中等 + self.assertIn("表格行数不一致", categories) # 3 vs 2 → 中等 + self.assertIn("版本号不一致", categories) # 1.26 vs 1.25 → 轻微 + # 严重排在最前 + self.assertEqual(findings[0].severity, "严重") + + def test_identical_docs_no_findings(self): + findings = compare_structures( + "a.md", "b.md", parse_structure(EN), parse_structure(EN) + ) + self.assertEqual(findings, []) + + def test_deterministic(self): + f1 = compare_structures("a", "b", parse_structure(EN), parse_structure(ZH)) + f2 = compare_structures("a", "b", parse_structure(EN), parse_structure(ZH)) + self.assertEqual([vars(f) for f in f1], [vars(f) for f in f2]) + + +class DiscoverTest(unittest.TestCase): + def test_discover(self): + names = ["README.md", "README.zh-CN.md", "LICENSE", "CONTRIBUTING.md"] + self.assertEqual(discover_pairs(names), [("README.md", "README.zh-CN.md")]) + + def test_no_pair(self): + self.assertEqual(discover_pairs(["README.md", "LICENSE"]), []) + + +class ReportTest(unittest.TestCase): + def test_report_contains_findings(self): + findings = compare_structures( + "README.md", "README.zh-CN.md", parse_structure(EN), parse_structure(ZH) + ) + report = render_report("o", "r", "master", [("README.md", "README.zh-CN.md", findings)]) + self.assertIn("🔴 严重", report) + self.assertIn("README.md ⇄ README.zh-CN.md", report) + + def test_report_clean(self): + report = render_report("o", "r", "", [("a.md", "b.md", [])]) + self.assertIn("无漂移", report) + + +if __name__ == "__main__": + unittest.main() -- 2.34.1 From 45ed4374bcf3f943bac055cc596b2502fa80026b Mon Sep 17 00:00:00 2001 From: farmyobutu5233 Date: Sun, 5 Jul 2026 10:43:24 +0000 Subject: [PATCH 2/3] =?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?=20=E5=B7=A5=E4=BD=9C=E6=B5=81=E8=A1=A5=E5=85=85=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E5=9B=BE=E4=B8=8E=20CI=20=E9=97=A8=E7=A6=81=E9=9B=86?= =?UTF-8?q?=E6=88=90=E7=A4=BA=E4=BE=8B?= 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 + .../workflows/doc-sync-automation/README.md | 35 ++++++++++++++++++ .../doc_sync_workflow.cpython-312.pyc | Bin 15533 -> 0 bytes 3 files changed, 36 insertions(+) delete mode 100644 examples/workflows/doc-sync-automation/scripts/__pycache__/doc_sync_workflow.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/workflows/doc-sync-automation/README.md b/examples/workflows/doc-sync-automation/README.md index 9382ac9..0cf906c 100644 --- a/examples/workflows/doc-sync-automation/README.md +++ b/examples/workflows/doc-sync-automation/README.md @@ -6,6 +6,41 @@ 与仓库内已有能力的关系:`file` 命令组(文件读写)→ `gitlink-doc-sync` Skill(AI 语义比对与翻译同步知识)→ **本工作流(确定性可复现闭环)**,三层互为支撑而非重复:Skill 负责需要语义理解的翻译同步,本工作流负责可进 CI 的确定性漂移检测。 +## 架构图 + +```mermaid +flowchart LR + A["repo +tree
仓库结构采集"] --> B["文档对自动发现
README.md ⇄ README.zh-CN.md
docs/*.md ⇄ docs/*.zh-CN.md"] + B --> C["file +view --raw ×2
拉取双语版本内容"] + C --> D["确定性结构比对
章节大纲 / 代码块 / 表格 / 版本号"] + D --> E["分级漂移报告
🔴严重 / 🟡中等 / 🟢轻微"] + E -->|"dry-run(默认)"| F["Markdown 报告落盘
退出码 0/2 → CI 门禁"] + E -->|"--apply"| G["issue +create
回写 tracking issue"] + G -.->|"人工确认后"| H["gitlink-doc-sync Skill
AI 语义翻译同步 → file +update → pr +create"] +``` + +## CI 门禁集成示例 + +利用退出码语义(`0` 无严重漂移 / `2` 存在严重漂移)可直接作为发布门禁。GitLink 引擎(`.gitea/workflows`)示例: + +```yaml +name: doc-sync-gate +on: + pull_request: + paths: ["README.md", "README.zh-CN.md", "docs/**"] +jobs: + doc-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm install -g @gitlink-ai/cli + - env: + GITLINK_TOKEN: ${{ secrets.GITLINK_TOKEN }} + run: | + python3 examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py \ + --owner ${{ github.repository_owner }} --repo ${{ github.event.repository.name }} +``` + ## 交付物 - `scripts/doc_sync_workflow.py`:文档对发现 + 漂移检测 + 报告 + tracking issue 回写(纯标准库,Python ≥3.9,零第三方依赖) diff --git a/examples/workflows/doc-sync-automation/scripts/__pycache__/doc_sync_workflow.cpython-312.pyc b/examples/workflows/doc-sync-automation/scripts/__pycache__/doc_sync_workflow.cpython-312.pyc deleted file mode 100644 index cae7398c13c740eb44c043e783e8d79413cc3432..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15533 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 a(Y}?(*1o#7v--r2`&0%X1u7RS8U7z}@|qt2 -- 2.34.1 From 8a3c56846b01b50698fa46034fbc33577af1de78 Mon Sep 17 00:00:00 2001 From: farmyobutu5233 Date: Sun, 5 Jul 2026 10:47:36 +0000 Subject: [PATCH 3/3] =?UTF-8?q?feat(doc-sync):=20=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=AF=B9=E5=8F=91=E7=8E=B0=E6=B3=9B=E5=8C=96=E4=B8=BA=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E5=91=BD=E5=90=8D=E7=BA=A6=E5=AE=9A=E5=B9=B6=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=20docs/=20=E7=9B=AE=E5=BD=95=EF=BC=8C=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E5=B5=8C=E5=A5=97/=E7=BF=BB=E8=AF=91=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=8D=95=E6=B5=8B?= 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> --- .../workflows/doc-sync-automation/README.md | 2 +- .../scripts/doc_sync_workflow.py | 33 +++++++++++-------- .../doc-sync-automation/tests/test_drift.py | 10 ++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/examples/workflows/doc-sync-automation/README.md b/examples/workflows/doc-sync-automation/README.md index 0cf906c..2b2b77e 100644 --- a/examples/workflows/doc-sync-automation/README.md +++ b/examples/workflows/doc-sync-automation/README.md @@ -71,7 +71,7 @@ python3 scripts/doc_sync_workflow.py --owner --repo --output-dir | 文档对自动发现 | 生产 gitlink.org.cn 真实仓库 | ✅ 自动识别 README.md ⇄ README.zh-CN.md | | 漂移检测 | 本仓库 README 双语版本 | ✅ 检出真实漂移:代码块 32 vs 29、表格 43 vs 40 行 | | `--apply` 真实回写 | 自有 fork | ✅ tracking issue 创建成功(issue #1,API 回执确认) | -| 单测 | `tests/test_drift.py` | ✅ 9/9 全绿 | +| 单测 | `tests/test_drift.py` | ✅ 11/11 全绿 | ## 设计要点 diff --git a/examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py b/examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py index 2d6e163..8cbfb18 100644 --- a/examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py +++ b/examples/workflows/doc-sync-automation/scripts/doc_sync_workflow.py @@ -19,12 +19,8 @@ import sys from dataclasses import dataclass, field from pathlib import Path -# 文档对命名约定:主文档 -> 可能的翻译文档 -PAIR_PATTERNS = [ - ("README.md", ["README.zh-CN.md", "README_zh.md", "README.zh.md", "README-zh.md"]), - ("CONTRIBUTING.md", ["CONTRIBUTING.zh-CN.md", "CONTRIBUTING_zh.md"]), - ("CHANGELOG.md", ["CHANGELOG.zh-CN.md"]), -] +# 翻译文档命名约定:主文档 X.md -> X{后缀} +TRANSLATION_SUFFIXES = [".zh-CN.md", "_zh.md", ".zh.md", "-zh.md"] SEVERITY_ORDER = {"严重": 0, "中等": 1, "轻微": 2} SEVERITY_ICON = {"严重": "🔴", "中等": "🟡", "轻微": "🟢"} @@ -66,10 +62,12 @@ def fetch_file(owner, repo, path, ref, cli="gitlink-cli"): return run_cli(args, cli=cli) -def list_root_entries(owner, repo, ref, cli="gitlink-cli"): +def list_entries(owner, repo, ref, path="", cli="gitlink-cli"): args = ["repo", "+tree", "--owner", owner, "--repo", repo, "--format", "json"] if ref: args += ["--ref", ref] + if path: + args += ["--path", path] out = run_cli(args, cli=cli) payload = json.loads(out) data = payload.get("data", payload) @@ -85,15 +83,17 @@ def list_root_entries(owner, repo, ref, cli="gitlink-cli"): def discover_pairs(names): - """按命名约定从文件名列表中发现文档对。""" + """按命名约定从文件名(可含路径前缀)列表中发现文档对。""" nameset = set(names) pairs = [] - for base, translations in PAIR_PATTERNS: - if base not in nameset: + for name in sorted(nameset): + if not name.endswith(".md") or any(name.endswith(s) for s in TRANSLATION_SUFFIXES): continue - for t in translations: - if t in nameset: - pairs.append((base, t)) + stem = name[: -len(".md")] + for suffix in TRANSLATION_SUFFIXES: + translation = stem + suffix + if translation in nameset: + pairs.append((name, translation)) break return pairs @@ -223,7 +223,12 @@ def main(): if args.pair: pairs = [tuple(p.split(":", 1)) for p in args.pair] else: - names = list_root_entries(args.owner, args.repo, args.ref, cli=args.cli) + names = list_entries(args.owner, args.repo, args.ref, cli=args.cli) + if "docs" in names: + names += [ + f"docs/{n}" + for n in list_entries(args.owner, args.repo, args.ref, path="docs", cli=args.cli) + ] pairs = discover_pairs(names) if not pairs: print("未发现双语文档对(可用 --pair 手动指定)", file=sys.stderr) diff --git a/examples/workflows/doc-sync-automation/tests/test_drift.py b/examples/workflows/doc-sync-automation/tests/test_drift.py index fbf651d..e3bef89 100644 --- a/examples/workflows/doc-sync-automation/tests/test_drift.py +++ b/examples/workflows/doc-sync-automation/tests/test_drift.py @@ -102,6 +102,16 @@ class DiscoverTest(unittest.TestCase): def test_no_pair(self): self.assertEqual(discover_pairs(["README.md", "LICENSE"]), []) + def test_discover_generic_and_nested(self): + names = ["docs/guide.md", "docs/guide.zh-CN.md", "USAGE.md", "USAGE_zh.md", "NOTES.zh.md"] + self.assertEqual( + discover_pairs(names), + [("USAGE.md", "USAGE_zh.md"), ("docs/guide.md", "docs/guide.zh-CN.md")], + ) + + def test_translation_file_not_treated_as_base(self): + self.assertEqual(discover_pairs(["README.zh-CN.md", "README_zh.md"]), []) + class ReportTest(unittest.TestCase): def test_report_contains_findings(self): -- 2.34.1