Files
gplan-2/tools/doc_term_audit.py
DKChan 6376356539 docs(通关口径统一): 「通关」全库退役——主体明确短语族替代歧义单义词:单关=关卡完成、整局=冒险成功、终结统称=冒险落幕(2026-10-26 用户裁)
改写范围:design/gdd 18+ 文件约 130 行非豁免正文+「冒险终结→冒险落幕」13 行+根件(体验验证清单/待办清单正文/AGENTS)。三类豁免不动:带日期历史行、待办条目标题(冻结登记元数据)、「」UI 引语(生涯页「通关局数」等玩家字样另按对外命名纪律单独裁)。术语表:「通关」「通过第 N 关」两行退役,新立关卡完成/冒险成功/冒险落幕三行,三选一行随改;退役词对照收通关/冒险终结。闸词表:doc_term_audit 收两词+通关的引语语境豁免,此后正文误用即机检拦截。特判三处:棋盘生成「可通关」=棋盘第三义改「可清图」;地形变异「双关同型」改「两关同型」;00-游戏概念 minesweeper 双关系 pun 义保留。计数器键名(adventure_cleared/level_clear_counts)与测量数据键零触碰。「通关口径混用」条目(2026-10-18 登记)完成即删。--docs 域闸全绿(退役词疑似现行 0)
2026-10-26 06:05:20 +08:00

244 lines
18 KiB
Python
Raw Permalink 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 伏击→警觉)之后,正文里容易残留旧措辞的
"现行语义"引用——修订记录与退役注记里的旧词是合法历史,混在一起肉眼难分。
本工具维护一份退役词表,全文档扫描命中并按上下文分两类:
[疑似现行] 该行不含日期/退役/换装等白名单信号——人工判读
[留档类] 行内带日期、退役/作废/换装/继任/留档/历史等信号——默认忽略
(变更记录类节自 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-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/hobgoblin/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())