Files
gplan-2/tools/doc_term_audit.py
T
DKChan 94f1bba840 fix(闸收敛批二): gdd_lint 分域豁免 + doc_term 语境豁免 + 孤儿探针退役
用户令四条:清单类与术语表免 GDD 规范性检查、退役词统一处理、文件头另处解决、
无使用的探针全删。

- tools/gdd_lint.py:新增 RULE_EXEMPT(规则粒度分域豁免,**按规则不按文件**)——
  06 免 R3/R4/R7、07 免 R3/R5/R7、术语表免 R2/R5。
  理由逐条写进代码注释:R3 禁修订注记与「登记处条目要能回答谁在何时裁的」直接冲突;
  R4 版本历史节与 06「解决即移除」的状态登记体裁冲突;R5 是节名含「流程」即触发的误匹配
  (07 的「㉖全流程走查」是目检清单节、术语表「开发流程」是词条节);
  R7 长括注在登记处是常态。
  **机械可判的完整性规则 R6/R8/R9/R10/R11 一律不豁免**,真缺陷不得藏起来。
- design/gdd/术语表.md:版本号撞号订正(10-12 那条 v1.2→v1.9,两条 10-13 顺延 v1.10/v1.11),
  按真实时序重排。原撞号源于 2026-10-11 与 10-12 两批各写一个 v1.2。
- tools/doc_term_audit.py:五组退役词按语境豁免而非改正文——
  充能(秘宝主动件现行机制,扩 exempt 信号)、可选类道具与雾格(否定式引用「现行池内无可选类」
  「不做清单」——替换会毁掉句意)、伏击/heavy_multiplier(battle.md 占位表里的退役敌人 id,
  现行规格在 敌人系统.md)、拔旗(06 待办「件派生动词白名单」是待评估设计意图非在用机制)。
  疑似现行 6 处 → 0。
- 孤儿探针退役:ui_layout_probe 与 ui_audit_probe 共 6 文件(.gd/.uid/.tscn)删除——
  两者均无任何闸脚本执行(全仓仅注释与历史报告提及)。
  连带清理 render_gate.sh 头注、board_view.gd cell_size 注释、game/README.md roster 行。
  **遗留**:ui_layout_probe 原承担 1920 档布局机检面,现无承载,已在 render_gate 头注标明待补。
- roadmap.md 迭代循环第 4/5 条:轻闸/重闸改指 run_all_gates.sh,闸清单不再散落第四处。

闸况:9 道绿(含 gdd_lint / doc_link / doc_term 三闸全绿);static_scan S10 文件头 125 处
按用户令不在本批处理,已落 baseline.json 作为存量红。
2026-10-15 21:42:35 +08:00

159 lines
9.2 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 复用)", "变体名未经批准退役批——用户质问查证后全仓清除"),
]
DEFAULT_DIRS = ["design/gdd", "design/registry"]
# 词级语境豁免:某词退役的只是特定机制语境,另一现行机制合法沿用同一词时,
# 命中行含豁免语境信号即不算退役词命中(不进疑似、不进留档计数)。
# 充能:挖掘期/战斗段充能退役(2026-09-19),但秘宝主动件「每冒险充能」(同调件
# 使用次数账,迭代 11 起在役)系现行机制共用该词——秘宝/同调/主动件等语境不算命中。
TERM_CONTEXT_EXEMPT = {
"充能": re.compile(r"秘宝|同调|主动件|每冒险|冒险开始|充能账|充能维护|attunement"
r"|incense_stick|sky_eye|reveal_tiles|战斗级充能|关卡级充能|每战一次|每关一次"),
# 否定式/反照式引用:正文在说明「该类别已退役/不做/不存在」,退役词出现在句中是
# 陈述本身的一部分,替换掉反而毁掉句意(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)
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())