From 7f935c2a2eb588c625d20e3770f31de8c78e4572 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 20 Sep 2026 04:35:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20=E9=80=80=E5=BD=B9=E6=9C=AF?= =?UTF-8?q?=E8=AF=AD=E6=89=AB=E6=8F=8F=E5=B7=A5=E5=85=B7=20doc=5Fterm=5Fau?= =?UTF-8?q?dit.py=EF=BC=88=E5=8D=8A=E8=87=AA=E5=8A=A8=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=E2=91=A0=E5=8A=A8=E6=9C=BA=3D=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E6=8D=A2=E5=90=8D/=E9=80=80=E5=BD=B9=E6=89=B9=E5=90=8E?= =?UTF-8?q?=E6=97=A7=E6=8E=AA=E8=BE=9E=E6=AE=8B=E7=95=99=E8=82=89=E7=9C=BC?= =?UTF-8?q?=E9=9A=BE=E5=88=86=EF=BC=88=E5=85=AB=E5=9E=8B=E6=89=B9=E5=AE=9E?= =?UTF-8?q?=E8=AF=81=EF=BC=9A=E6=A0=87=E7=A9=BA=E6=97=97=E4=B8=89=E9=80=89?= =?UTF-8?q?=E4=B8=80=E3=80=81=E6=8B=94=E6=97=97=E3=80=81=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=E7=8E=AF=E7=AD=89=2011+=20=E5=A4=84=E6=BC=8F=E7=BD=91=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E2=91=A1=E8=AE=BE=E8=AE=A1=3D=E9=80=80=E5=BD=B9?= =?UTF-8?q?=E8=AF=8D=E8=A1=A8=2018=20=E6=9D=A1=EF=BC=88=E4=BC=8F=E5=87=BB/?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E6=9C=AA=E7=94=9F=E6=95=88/=E4=B8=83?= =?UTF-8?q?=E5=9E=8B/=E6=8B=94=E6=97=97/=E8=93=84=E5=8A=9B=E8=A1=8C/?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E7=8E=AF/=E9=81=93=E5=85=B7=E7=AA=97?= =?UTF-8?q?=E5=8F=A3/=E9=9B=BE=E6=A0=BC=E2=80=A6=E5=90=84=E5=B8=A6?= =?UTF-8?q?=E9=80=80=E5=BD=B9=E6=97=A5=E6=9C=9F+=E7=BB=A7=E4=BB=BB?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=EF=BC=89=C3=97=E6=89=AB=E6=8F=8F=E5=9F=9F=20?= =?UTF-8?q?design/{gdd,registry,ux}=C3=97=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E4=BA=8C=E5=88=86=E7=B1=BB=E2=80=94=E2=80=94=E8=A1=8C=E7=BA=A7?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E4=BF=A1=E5=8F=B7=EF=BC=88=E6=97=A5?= =?UTF-8?q?=E6=9C=9F/=E9=80=80=E5=BD=B9/=E4=BD=9C=E5=BA=9F/=E6=8D=A2?= =?UTF-8?q?=E8=A3=85/=E7=BB=A7=E4=BB=BB/=E7=95=99=E6=A1=A3/v0.x/=E5=8F=B0?= =?UTF-8?q?=E8=B4=A6=EF=BC=89+=E7=89=88=E6=9C=AC=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=8A=82=E6=95=B4=E6=AE=B5=E8=B1=81=E5=85=8D+=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=202=20=E8=A1=8C=E7=95=99=E6=A1=A3=E5=A4=B4=E6=B3=A8?= =?UTF-8?q?=E7=BB=A7=E6=89=BF=EF=BC=88'=E4=B8=8B=E8=A1=A8=E7=95=99?= =?UTF-8?q?=E6=A1=A3=E4=BD=9C=E5=BA=9F'=E6=83=85=E5=BD=A2=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E2=91=A2=E7=94=A8=E6=B3=95=3D=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E9=BB=98=E8=AE=A4=20exit=200=20/=20--strict?= =?UTF-8?q?=20=E4=BE=9B=E9=97=A8=E7=A6=81=20exit=201=20/=20--json=20?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E5=8F=AF=E8=AF=BB=EF=BC=9B=E8=AF=8D=E8=A1=A8?= =?UTF-8?q?=E6=89=A9=E5=85=85=3D=E6=94=B9=20RETIRED=5FTERMS=20=E4=B8=80?= =?UTF-8?q?=E5=A4=84=EF=BC=9B=E2=91=A3=E9=A6=96=E8=B7=91=E8=AF=BB=E6=95=B0?= =?UTF-8?q?=3D31=20=E6=96=87=E4=BB=B6=20225=20=E5=91=BD=E4=B8=AD=E3=80=81?= =?UTF-8?q?=E7=96=91=E4=BC=BC=E7=8E=B0=E8=A1=8C=2016=20=E5=A4=84=EF=BC=88?= =?UTF-8?q?=E7=9C=9F=E6=AE=8B=E7=95=99=E7=BA=A6=2011=20=E5=A4=84=EF=BC=9A?= =?UTF-8?q?=E4=BA=94=E5=9E=8B=C3=97=E6=AD=A3=E8=B4=9F/=E6=8B=94=E6=97=97?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E8=A1=8C/=E6=A0=87=E9=94=99=E5=8F=8D?= =?UTF-8?q?=E5=99=AC=E4=B8=A4=E5=A4=84/=E8=A1=8C=E4=B8=BA=E7=8E=AF?= =?UTF-8?q?=E4=B8=A4=E5=A4=84/=E9=81=93=E5=85=B7=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E4=BA=94=E5=A4=84/=E5=B7=B2=E6=B6=88=E8=80=97=E6=9C=AA?= =?UTF-8?q?=E7=94=9F=E6=95=88=E6=9E=9A=E4=B8=BE/=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E9=94=9A=E8=A1=8C=E4=BE=8B=E4=B8=BE=EF=BC=89=EF=BC=8C=E7=95=99?= =?UTF-8?q?=E6=A1=A3=E7=B1=BB=20209=20=E5=A4=84=E8=87=AA=E5=8A=A8=E5=BF=BD?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/doc_term_audit.py | 130 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tools/doc_term_audit.py diff --git a/tools/doc_term_audit.py b/tools/doc_term_audit.py new file mode 100644 index 00000000..a3a44589 --- /dev/null +++ b/tools/doc_term_audit.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +r"""doc_term_audit.py — GDD 退役术语扫描(半自动) + +动机:机制换名/退役批(如 2026-09-20 伏击→警觉)之后,正文里容易残留旧措辞的 +"现行语义"引用——修订记录与退役注记里的旧词是合法历史,混在一起肉眼难分。 +本工具维护一份退役词表,全文档扫描命中并按上下文分两类: + [疑似现行] 该行不含日期/退役/换装等白名单信号,也没落在版本历史节——人工判读 + [留档类] 行内带日期、退役/作废/换装/继任/留档/历史等信号,或位于修订记录节——默认忽略 + +用法: + python tools/doc_term_audit.py # 扫默认范围,报告模式(exit 0) + python tools/doc_term_audit.py --strict # 有疑似现行命中时 exit 1(供门禁/CI) + python tools/doc_term_audit.py ... # 只扫指定文件 + python tools/doc_term_audit.py --json # 机器可读 + +口径:只扫 design/gdd/ design/registry/ design/ux/——design/archive、design/research +(历史提案)、docs/(样例)与 production/ 不在扫描域;词表扩充 = 改 RETIRED_TERMS。 +""" +import argparse +import json +import re +import sys +from pathlib import Path + +# 退役词表:(正则词, 退役日期, 继任词/口径, 备注)。新增退役批时在此追加一行。 +RETIRED_TERMS = [ + ("伏击", "2026-09-20", "警觉(blast 负向)", "伏击机制整体退役"), + ("ambush_double|ambush_layers|flat_attach", "2026-09-20", "enemy_alert / effects 投影", "伏击数据键退役"), + ("advantage_per_round_cap", "2026-09-20", "(已删字段——行动点天然封顶)", "单轮授予上限删除批"), + ("溢出未生效", "2026-09-20", "(行类型退役;满层拒收走奖励未生效行)", "行协议 10→9 类"), + ("m_amb|M_AMB", "2026-09-20", "(伏击暴露当量口径作废)", "挖掘风险不等式退役"), + ("七型|五型×正负|五型 × 正负", "2026-09-20", "八型(十六格)", "格面资源八型批"), + ("10 类行|行型封闭为 10 类|10 类封闭协议|行型 10 类", "2026-09-20", "9 类", "溢出未生效行下线"), + ("拔旗|撤旗", "2026-09-19", "(旗无移除手段——插旗不可逆)", "插旗不可逆批"), + ("三选一反噬|标错反噬|反噬三选一", "2026-09-15", "极性矩阵负向兑付", "格面资源批"), + ("defense_up", "2026-09-16", "ac_up(护甲强化)", "护甲等级掷点批换装"), + ("蓄力行|charge 行", "2026-09-16", "施放行(cast)", "敌人技能池批改型"), + ("行为环", "2026-09-16", "技能池(池模板+摇号)", "固定出招剧本退役"), + ("heavy_multiplier|重击倍率", "2026-09-16", "技能行 damage_mult", "行为环退役随批"), + ("多段命中", "2026-09-19", "(退役,无防御磨损体系)", "技能池新效应词汇批"), + ("充能", "2026-09-19", "(挖掘期/战斗段充能退役;蓄力 empower 仍在)", "充能退役批——注意误报"), + ("英雄卡|敌人卡", "2026-09-20", "站桩立绘", "敌人卡形态裁决批"), + ("道具窗口|可选类道具|已消耗未生效|使用被拒|窗口打开锚行|窗口跳过锚行", "2026-09-15", "(随可选类道具整体删除)", "格面资源批"), + ("雾格", "2026-09-14", "(雾格移除)", "不做清单"), +] + +DEFAULT_DIRS = ["design/gdd", "design/registry", "design/ux"] + +# 行级白名单信号:命中任一即归"留档类" +WHITELIST_RE = re.compile( + r"2026-\d{2}-\d{2}" + r"|退役|作废|换装|继任|留档|历史口径|已下线|随批|随伏击|不再|已删除" + r"|\bv0\.\d+" + r"|修订记录|版本历史|履行回执|台账" +) +# 节级白名单:版本历史/修订记录节内整段豁免 +SECTION_RE = re.compile(r"^#{1,6}\s*(版本历史|修订记录)") +COMMENT_RE = re.compile(r"^\s*[;#]") + + +def scan(paths): + terms = [(re.compile(t), date, repl, note) for t, date, repl, note in RETIRED_TERMS] + findings = [] # {term, file, line, text, klass, retired, replacement} + for p in paths: + text = Path(p).read_text(encoding="utf-8").replace("\r\n", "\n") + in_version_section = False + prev_lines = [] # 已处理行(供"上一 2 行留档标记继承"判读) + for lineno, line in enumerate(text.split("\n"), 1): + if SECTION_RE.match(line): + in_version_section = True + hits = [(m.group(0), t) for t in terms for m in [t[0].search(line)] if m] + # 上一 2 行带留档/作废/历史标记(如"下表留档作废"头注)→ 本行视为留档 + prev = " ".join(prev_lines[-2:]) if prev_lines else "" + prev_lines.append(line) + if not hits: + continue + if in_version_section or WHITELIST_RE.search(line) or WHITELIST_RE.search(prev) or COMMENT_RE.match(line): + klass = "留档类" + else: + klass = "疑似现行" + findings.append({ + "term": "、".join(sorted({h[0] for h in hits})), + "file": str(p).replace("\\", "/"), + "line": lineno, + "klass": klass, + "text": line.strip()[:160], + "retired": hits[0][1][1], + "replacement": hits[0][1][2], + }) + return findings + + +def main(): + ap = argparse.ArgumentParser(description="GDD 退役术语扫描(半自动)") + ap.add_argument("files", nargs="*", help="指定文件;缺省扫默认目录") + ap.add_argument("--strict", action="store_true", help="疑似现行命中时 exit 1") + ap.add_argument("--json", action="store_true") + args = ap.parse_args() + + if args.files: + paths = args.files + else: + paths = [] + for d in DEFAULT_DIRS: + paths += sorted(Path(d).rglob("*.md")) + paths += sorted(Path(d).rglob("*.yaml")) + paths = [p for p in paths if Path(p).exists()] + + findings = scan(paths) + suspect = [f for f in findings if f["klass"] == "疑似现行"] + kept = [f for f in findings if f["klass"] != "疑似现行"] + + if args.json: + print(json.dumps({"suspect": suspect, "archived_count": len(kept)}, ensure_ascii=False, indent=1)) + else: + print(f"扫描 {len(paths)} 个文件 · 退役词命中 {len(findings)} 处 · 疑似现行 {len(suspect)} 处(留档类 {len(kept)} 处默认忽略)") + by_term = {} + for f in suspect: + by_term.setdefault(f["term"], []).append(f) + for term, items in sorted(by_term.items()): + print(f"\n【{term}】×{len(items)}(退役 {items[0]['retired']} → {items[0]['replacement']})") + for it in items[:8]: + print(f" {it['file']}:{it['line']} {it['text']}") + if len(items) > 8: + print(f" …另有 {len(items) - 8} 处") + return 1 if (args.strict and suspect) else 0 + + +if __name__ == "__main__": + sys.exit(main())