247 lines
18 KiB
Python
247 lines
18 KiB
Python
#!/usr/bin/env python3
|
||
r"""doc_term_audit.py — GDD 退役术语扫描(半自动)
|
||
|
||
动机:机制换名/退役批(如 2026-09-20 伏击→警觉)之后,正文里容易残留旧措辞的
|
||
"现行语义"引用——修订记录与退役注记里的旧词是合法历史,混在一起肉眼难分。
|
||
本工具维护一份退役词表,全文档扫描命中并按上下文分两类:
|
||
[疑似现行] 该行不含日期/退役/换装等白名单信号——人工判读
|
||
[留档类] 行内带日期、退役/作废/换装/继任/留档/历史等信号——默认忽略
|
||
(变更记录类节自 2026-10-19 起由 gdd_lint R4 禁设,本工具不再为其设节级豁免)
|
||
|
||
用法:
|
||
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/art/ 两处文档域 + 四份根文件(`待办清单.md`/`体验验证清单.md`/
|
||
`roadmap.md`/`AGENTS.md`,2026-10-21 迭代 32 卡①反哺批扩面,用户裁;根文件同属现行文本)
|
||
——reports/(中间文档唯一暂存地)不在扫描域;design/ 全目录已无过程件目录(research 于 2026-10-21 退役);
|
||
词表扩充 = 改 RETIRED_TERMS(受 doc-conventions §4 词表冻结约束:唯一入口=用户实读反馈并经用户裁决)。
|
||
"""
|
||
import argparse
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 退役词表:(正则词, 退役日期, 继任词/口径, 备注)。新增退役批时在此追加一行。
|
||
RETIRED_TERMS = [
|
||
(r"registry|entities\.yaml|architecture\.yaml", "2026-10-19", "术语规范名→design/gdd/术语表.md;现行值→data/tables/*.tres;取值域与字段契约→data/schemas/*.gd;架构立场→design/engineering/architecture/architecture.md", "跨系统事实登记处注销批(2026-10-19 用户裁)"),
|
||
("伏击", "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-10-18 术语刷新随批换代措辞)"),
|
||
("三选一反噬|标错反噬|反噬三选一", "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 表,机器闸吸收 ——
|
||
# 词表冻结:本节禁再新增词条;扩表唯一入口=用户实读反馈并经用户裁决,由非迭代批写入,agent 禁自行增词
|
||
# (doc-conventions §4「词表冻结」)。
|
||
# 注意:「随批」「台账」两词本身是上方行级白名单信号(行内出现即归留档类),对其扫描会自豁免,
|
||
# 故不列入;「收口」「在飞」等照常扫描。术语表「用语规范」节已加入节级豁免(词表自身不命中)。
|
||
("对标", "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 节同族)"),
|
||
# —— 设计文档术语刷新批(2026-10-18 用户裁):权威=design/gdd/术语表.md「退役词对照」——
|
||
# GP 是唯一正式命名(「金币」系 AI 噪音);「旗」概念不存在(动作=封印、印记=封印印记,
|
||
# 封印系正式术语非比喻)。注意:「插旗|标旗」与上方「拔旗|撤旗」行同族;「旗」单字不入表
|
||
# (词面误报率高,靠术语表对照行与人工判读),game/ 扫描域归代码批后另加。
|
||
("金币", "2026-10-18", "GP(唯一正式命名)", "GP 命名统一批——「金币」全库禁用"),
|
||
("插旗|标旗", "2026-10-18", "封印(正式动作名,全库统一)", "术语刷新批——「旗」概念退役"),
|
||
("完美排雷", "2026-10-19", "完美封印(正式名见 design/gdd/术语表.md 退役词对照)", "迭代 32 卡①反哺批 2026-10-21 用户裁入表——代码注释面由 S11 承接,文档正文面由此条承接"),
|
||
# —— 测试术语与层级重构批(2026-10-22 用户裁):权威=design/gdd/术语表.md「退役词对照」——
|
||
# 标准测试层(冒烟/单元/系统)+UI 探针与 golden;闸档用语随三条频率规则消亡。
|
||
# 「重闸」用负向后行断言排除机制词「去重闸」(封印重判去重判别器,在役非退役)。
|
||
("系统级断言套件|冒烟全量", "2026-10-22", "系统测试", "测试术语重构批——标准四级层命名(裁决录=提案 §二——提案已退役,考古=git 历史——删除批 53075baa,删除前全稿见历史版本 b4da36a8)"),
|
||
("单元套件", "2026-10-22", "单元测试", "测试术语重构批——标准四级层命名"),
|
||
("启动探针|秒级冒烟探针|L0[–-]L3", "2026-10-22", "冒烟测试", "测试术语重构批——极小启动场景"),
|
||
(r"(?<!去)重闸|轻闸|三查", "2026-10-22", "(随频率规则消亡:检出工作区零测试;卡内只测自己改的东西;全量只有批末+合回两个时机)", "测试术语重构批——闸档用语退役;代码注释面由 S11 承接(S11 设「去重闸」同形异义豁免)"),
|
||
# —— 测试层合并批(2026-11-07 用户裁):跨系统真实路径闭环套件归单元层,三套件实体迁移 tests/unit/;
|
||
# 测试层识别三元组=职责×触发时间点×覆盖面积(test-conventions「测试层三元组」节),层级名只是代称。
|
||
("集成测试|集成用例|集成层|集成侧|集成宿主|集成族", "2026-11-07", "单元测试(跨系统真实路径闭环套件归单元层,性质归属由套件头注承载)", "测试层合并批——「集成」层级词退役,文档正文面由此条承接,代码注释面由 S11 承接"),
|
||
# —— 旧词残留清扫批(2026-10-26 用户裁):权威=design/gdd/术语表.md「退役词对照」——
|
||
# 魔法道具=秘宝旧中文规范名(2026-10-05 消歧更名);罕见=旧三档档位名,现行四级第三档=稀有
|
||
# (档名映射见 adventure_table.gd 头注)——散文形容词义的「罕见」人工判读,不算档位名残留。
|
||
("魔法道具", "2026-10-05", "秘宝(AUXILIARY_ITEMS 中文规范名)", "消歧更名批;2026-10-26 用户裁入表防再犯(旧词残留清扫批)"),
|
||
("罕见", "2026-10-26", "稀有(四级档名第三档——旧三档名换代);散文形容词义人工判读", "档位旧名退役(旧词残留清扫批 2026-10-26 用户裁入表)"),
|
||
# —— 通关口径统一批(2026-10-26 用户裁):权威=design/gdd/术语表.md「退役词对照」——
|
||
# 主体明确短语族替代歧义单义词:单关=关卡完成、整局=冒险成功、终结统称=冒险落幕。
|
||
# 「」引语内的「通关×」是 UI 引语(玩家可见字样另按对外命名纪律裁),见 TERM_CONTEXT_EXEMPT。
|
||
("通关", "2026-10-26", "关卡完成(单关)/冒险成功(整局)", "通关口径统一批——主体明确短语族替代歧义单义词(2026-10-26 用户裁)"),
|
||
("冒险终结", "2026-10-26", "冒险落幕", "通关口径统一批——终结事件词随批换代"),
|
||
]
|
||
|
||
DEFAULT_DIRS = ["design/gdd", "design/art"]
|
||
# 根规则与流程文件同属现行文本,2026-10-21 迭代 32 反哺批复测后纳入扫描面:
|
||
# 当日实测这批文件自身含 3 处用语表废止词(roadmap 的「对拍」「标旗」、AGENTS 的「收口」×2),
|
||
# 随同批订正后判红归零才扩面——先改现实、再让闸盯住现实,避免带红扩面。
|
||
DEFAULT_FILES = ["待办清单.md", "体验验证清单.md", "roadmap.md", "AGENTS.md"]
|
||
|
||
# 用语规范 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/bugbear/giant_spider/drider/duergar 系,占位行里的退役
|
||
# 敌人 id 只能读作历史留档(真正的现行规格在 敌人系统.md「目标顺位与意图线索」节)。
|
||
"伏击": re.compile(r"ambusher|成文占位"),
|
||
"heavy_multiplier|重击倍率": re.compile(r"ambusher|heavy_multiplier|成文占位"),
|
||
# 拔旗(2026-09-19 退役「旗无移除手段」):待办清单「件派生动词白名单」条目说的是
|
||
# 「将来件池 ≥30 种后可能开放的第三盘面动作」,是待评估的设计意图而非在用机制。
|
||
"拔旗|撤旗": re.compile(r"件派生动词白名单|第三盘面动作|非信息类"),
|
||
# 通关(2026-10-26 退役):「」引语内的「通关×」是 UI 引语——玩家可见字样不属本闸管辖
|
||
# (另按对外命名纪律单独裁),不算正文残留。
|
||
"通关": re.compile(r"「[^」]*通关[^」]*」"),
|
||
}
|
||
|
||
# 行级白名单信号:命中任一即归"留档类"(变更记录类词面不入白名单——此类节自 2026-10-19
|
||
# 起由 gdd_lint R4 禁设,出现即违规,不得借白名单隐身)
|
||
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 += [Path(f) for f in DEFAULT_FILES]
|
||
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())
|