Files
gplan-2/tools/doc_term_audit.py
T

205 lines
12 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"]
# 用语规范 A 表补充词专项补扫:随批/台账 两词本身是上方行级白名单信号(行内出现即归
# 留档类),主扫描对它们自豁免;本通道只做版本历史节外的专项报告,供后续触碰批清偿。
SUPPLEMENT_TERMS = [
("随批", "2026-10-15", "顺带", "用语规范 A 表(doc-conventions 第 4 节)"),
("台账", "2026-10-15", "状态记录", "用语规范 A 表(doc-conventions 第 4 节)"),
]
def scan_supplement(paths):
terms = [(re.compile(t), date, repl, note) for t, date, repl, note in SUPPLEMENT_TERMS]
findings = []
for p in paths:
text = Path(p).read_text(encoding="utf-8").replace("\r\n", "\n")
in_version_section = False
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]
if not hits or in_version_section or COMMENT_RE.match(line):
continue
findings.append({
"term": "、".join(sorted({h[0] for h in hits})),
"file": str(p).replace("\\", "/"),
"line": lineno,
"klass": "疑似现行",
"text": line.strip()[:160],
"retired": hits[0][1][1],
"replacement": hits[0][1][2],
})
return findings
# 词级语境豁免:某词退役的只是特定机制语境,另一现行机制合法沿用同一词时,
# 命中行含豁免语境信号即不算退役词命中(不进疑似、不进留档计数)。
# 充能:挖掘期/战斗段充能退役(2026-09-19),但秘宝主动件「每冒险充能」(同调件
# 使用次数账,迭代 11 起在役)系现行机制共用该词——秘宝/同调/主动件等语境不算命中。
TERM_CONTEXT_EXEMPT = {
"充能": re.compile(r"秘宝|同调|主动件|每冒险|冒险开始|充能账|充能维护|attunement"
r"|incense_stick|sky_eye|reveal_tiles|战斗级充能|关卡级充能|每战一次|每关一次"),
# buff:反引号行内代码(字段/键名)原样保留(STC 6.2),只查散文用词
r"\bbuff\b": re.compile(r"`[^`]*buff[^`]*`"),
# 否定式/反照式引用:正文在说明「该类别已退役/不做/不存在」,退役词出现在句中是
# 陈述本身的一部分,替换掉反而毁掉句意(2026-10-15 用户令「统一替换」问询的判定)。
"道具窗口|可选类道具|已消耗未生效|使用被拒|窗口打开锚行|窗口跳过锚行": re.compile(
r"无可选类|不存在|已随|整体删除|现行池内|不进背包"),
"雾格": re.compile(r"不做清单|战争迷雾类机制"),
# heavy_multiplier 字段与 ambusher 敌人行同于 2026-09-20 八型批整体退役(enemy_table.gd
# 与 enemy.tres 均零命中);battle.md「敌人意图线索映射目录」是 2026-09-12 成文占位表,
# 现行敌人为 goblin/hobgoblin/bugbear/giant_spider/drider/duergar 系,占位行里的退役
# 敌人 id 只能读作历史留档(真正的现行规格在 敌人系统.md「目标顺位与意图线索」节)。
"伏击": re.compile(r"ambusher|成文占位"),
"heavy_multiplier|重击倍率": re.compile(r"ambusher|heavy_multiplier|成文占位"),
# 拔旗(2026-09-19 退役「旗无移除手段」):06 待办清单「件派生动词白名单」条目说的是
# 「将来件池 ≥30 种后可能开放的第三盘面动作」,是待评估的设计意图而非在用机制。
"拔旗|撤旗": re.compile(r"件派生动词白名单|第三盘面动作|非信息类"),
}
# 行级白名单信号:命中任一即归"留档类"
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) + scan_supplement(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())