Files
gplan-2/tools/doc_term_audit.py
T

159 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <file.md> ... # 只扫指定文件
python tools/doc_term_audit.py --json # 机器可读
口径:只扫 design/gdd/ design/registry/——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", "(雾格移除)", "不做清单"),
("兄弟精", "2026-09-26", "L2 熊地精(bugbear 复用)", "变体名未经批准退役批——用户质问查证后全仓清除"),
# —— 用语规范(2026-10-15 用户令):权威=design/gdd/术语表.md「用语规范」A 表,机器闸吸收 ——
# 注意:「随批」「台账」两词本身是上方行级白名单信号(行内出现即归留档类),对其扫描会自豁免,
# 故不列入;「收口」「在飞」等照常扫描。术语表「用语规范」节已加入节级豁免(词表自身不命中)。
("对标", "2026-10-15", "参照", "用语规范 A 表(STC 1.7)"),
("若干", "2026-10-15", "写具体数值或写明数值出处", "用语规范 A 表(STC 5.4)"),
("不得", "2026-10-15", "禁止", "用语规范 A 表(STC 词表)"),
("报错", "2026-10-15", "名词用错误;动词用返回错误/显示错误", "用语规范 A 表(STC 词表)"),
(r"\bbuff\b", "2026-10-15", "增益", "用语规范 A 表(STC 1.1)"),
("收口", "2026-10-15", "完成/合并进 master", "用语规范 A 表(doc-conventions 第 4 节)"),
("在飞", "2026-10-15", "正在执行中", "用语规范 A 表(doc-conventions 第 4 节)"),
("对拍", "2026-10-15", "与基线数据逐项比对", "用语规范 A 表(doc-conventions 第 4 节)"),
("销账|销号", "2026-10-15", "注销", "用语规范 A 表(doc-conventions 第 4 节)"),
("落文", "2026-10-15", "写入文档/已写入", "用语规范 A 表(doc-conventions 第 4 节同族)"),
]
DEFAULT_DIRS = ["design/gdd", "design/registry"]
# 词级语境豁免:某词退役的只是特定机制语境,另一现行机制合法沿用同一词时,
# 命中行含豁免语境信号即不算退役词命中(不进疑似、不进留档计数)。
# 充能:挖掘期/战斗段充能退役(2026-09-19),但秘宝主动件「每冒险充能」(同调件
# 使用次数账,迭代 11 起在役)系现行机制共用该词——秘宝/同调/主动件等语境不算命中。
TERM_CONTEXT_EXEMPT = {
"充能": re.compile(r"秘宝|同调|主动件|每冒险|冒险开始|充能账|充能维护|attunement"),
# buff:反引号行内代码(字段/键名)原样保留(STC 6.2),只查散文用词
r"\bbuff\b": re.compile(r"`[^`]*buff[^`]*`"),
}
# 行级白名单信号:命中任一即归"留档类"
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]
hits = [
(word, t) for (word, t) in hits
if not (t[0].pattern in TERM_CONTEXT_EXEMPT and TERM_CONTEXT_EXEMPT[t[0].pattern].search(line))
]
# 上一 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())