Files
gplan-2/tools/coverage_enum.py
T
DKChan 8b09b2c952 追红(远端 run 95 两条红转绿·修复车道): 红因1=判定级——能力审计 G 道「oracle 注记函数 401·红项 1」, 红项原文「game/probe/probe_coverage_recorder.gd:201 验证函数 dump_js 缺 # oracle 注记」(dump_js 体内三处 push_error( 命中 capability_audit.py:341 VERDICT_RE → 属须注记的验证函数, :386 判红); 上一批「判据出处注记 402 函数全库铺开」的跑绿读数靠主工作区一行未提交注记撑过, 未随批 commit, 故本地 strict 绿而远端干净检出必红; 本批在该文件补同一行注记(放法照仓内既有形态——函数定义行下一行顶格, 样本 camp_scene_layout_test.gd:13/:18, 指针 design/engineering/test-conventions.md:30 与主区工作稿逐字节同, cmp 已核)。红因2=非判定级但在日志里成串报红——tools 四处五 sites 用 Path.write_text(..., newline="\n"), 该关键字 Python 3.13+ 才有(本机 3.13.14 绿), Mac runner 的 python3 更老 → TypeError("write_text() got an unexpected keyword argument 'newline'") → 六层明细回执在远端一件未落盘, run 93/95 日志同报此错(挂点按设计是观测件不翻闸判, 故它不是 run 95 的失败主因, 但等于覆盖总览面板的六层明细一直是空的); 五处改 open(str(p), "w", encoding="utf-8", newline="\n") 写法(newline 形参 builtin open 自 3.0 起有, LF 纪律不丢), 覆盖面=coverage_enum.py:148/coverage_receipts.py:160/:179/extract_capability_list.py:104/extract_checks.py:588; 注: tools/t17_fanout2_rebuild.py:140 与 t5_build_templates.py:237 本来就是 open(..., newline=) 形态, 不属坏件未动。本步凭据(全部区内实跑): ①修复前在干净检出复现红——capability_audit --strict 出「401 注记·红项 1」并打出上述红项原文; ②修复后同件 exit=0「402 注记·红项 0」; ③四件 py_compile OK 且 grep 全仓 write_text(.*newline= 零残留; ④runner 语义实证——把 Path.write_text 垫片成 3.12 行为(newline 一律 TypeError)再跑 coverage_receipts.main(), 六层与 probe_manifest 全部正常落盘、无一行「[receipts] 红」, 产物字节核对 gate_probe/gate_render/gate_smoke.js 均 CRLF=0 LF=14(行尾纪律未破); ⑤run_all_gates --docs 验收通过无新增红(基线 9e81408e)。引擎层本地未跑: 区内 --premerge 被素材齐盘预检拦在冷检出态(缺 .godot/uid_cache.bin 与字体 .import 边车, 处置需从主区拷素材再 --editor --quit 刷导入), 本批五处改动零游戏行为面, 引擎全套件由主区合回时的 pre-merge-commit 前置检查跑, 全量档归合回后远端 CI
2026-11-06 23:47:28 +08:00

160 lines
7.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
# -*- coding: utf-8 -*-
r"""coverage_enum.py — 本体枚举器(游戏内容↔测试验证对账,B 面板·义务层双源之机器源)
两个文本扫描器+一次比对,全程不依赖跑游戏(交互命中维度除外——由探针覆盖登记另行承载):
扫描器 1(游戏本体枚举)——「游戏有什么」:
game/data/tables/*.tres 行集主键(`"xxx_id": "yyy"` 形态,逐表逐行)
game/data/schemas/*.gd 词表常量与字段名(const 块内的引号 token+var 字段名)
game/presentation/**/*.tscn BaseButton 族节点(scene+节点名)
game/presentation/**/*.gd _gui_input/_input 自绘命中类(脚本名)
game/presentation/views/*.gd 屏与模态清单(*_screen/*_overlay/*_panel 视图类)
扫描器 2(验证关联)——「验证了什么」:复用 tools/extract_checks.py 的断言静态提取
(unit/**+integration/**+system_test.gd 全量),实体 id 在「断言标签+cond+源文件+函数」
文料中出现即记一次关联。
比对=差集:本体存在、验证零关联=缺口清单,按类别分组、标首见批次(本工具生成时间)。
诚实边界(契约 §7 在案):grep 关联有假阳假阴——断言测了实体但文本没带 id=假缺口(人工核销
兜);文本带 id 但断言弱=假覆盖(oracle 改造堵)。推荐约定「断言文本含被测实体 id」提高精度,
不立规约。交互点(按钮/物件被点过没有)不在本工具判据内——那归探针真输入覆盖登记。
用法: python tools/coverage_enum.py [--out <路径>]
产出: game/tests/.gates/coverage/ontology_gaps.js(JSONP,B 面板消费)
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
GAME = ROOT / "game"
DEFAULT_OUT = GAME / "tests" / ".gates" / "coverage" / "ontology_gaps.js"
TRES_ID_RE = re.compile(r'"(\w+_id)":\s*"([^"]+)"')
SCHEMA_TOKEN_RE = re.compile(r'"([a-z][a-z0-9_]{2,40})"')
SCHEMA_VAR_RE = re.compile(r"^@?export\s+var\s+(\w+)\s*:", re.M)
TSCN_NODE_RE = re.compile(r'\[node name="([^"]+)"[^>]*type="(\w*Button\w*)"')
GUI_INPUT_RE = re.compile(r"^func (_gui_input|_input)\(", re.M)
VIEW_KIND_RE = re.compile(r"(screen|overlay|panel)$")
def scan_tables() -> list[dict]:
out: list[dict] = []
tables = GAME / "data" / "tables"
for f in sorted(tables.glob("*.tres")):
text = f.read_text(encoding="utf-8", errors="replace")
seen: set[tuple[str, str]] = set()
for m in TRES_ID_RE.finditer(text):
key = (m.group(1), m.group(2))
if key not in seen:
seen.add(key)
out.append({"category": "表行:%s" % f.stem, "id": m.group(2), "id_field": m.group(1)})
return out
def scan_schemas() -> list[dict]:
out: list[dict] = []
schemas = GAME / "data" / "schemas"
for f in sorted(schemas.glob("*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
tokens = set(SCHEMA_TOKEN_RE.findall(text))
fields = set(SCHEMA_VAR_RE.findall(text))
for t in sorted(tokens):
out.append({"category": "词表/枚值:%s" % f.stem, "id": t, "id_field": "token"})
for fl in sorted(fields):
out.append({"category": "schema字段:%s" % f.stem, "id": fl, "id_field": "field"})
return out
def scan_tscn_buttons() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.tscn")):
text = f.read_text(encoding="utf-8", errors="replace")
for m in TSCN_NODE_RE.finditer(text):
out.append({"category": "场景按钮", "id": "%s:%s" % (f.name, m.group(1)), "id_field": m.group(2)})
return out
def scan_gui_input_classes() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
kinds = set(GUI_INPUT_RE.findall(text))
for k in kinds:
out.append({"category": "自绘命中类", "id": f.stem, "id_field": k})
break # 一类一行(一个脚本同具两面也只记一次)
return out
def scan_screens() -> list[dict]:
out: list[dict] = []
for f in sorted((GAME / "presentation" / "views").glob("*.gd")):
if VIEW_KIND_RE.search(f.stem):
out.append({"category": "屏与模态", "id": f.stem, "id_field": "view"})
return out
def assertion_corpus() -> tuple[list[dict], str]:
sys.path.insert(0, str(ROOT / "tools"))
import extract_checks as ec # noqa: PLC0415 —— 复用现役提取机制(契约 §7 指定扩面)
rows, _problems = ec.extract_all(ec.default_sources())
parts: list[str] = []
for r in rows:
parts.append("%s %s %s %s" % (r.get("src", ""), r.get("func", ""),
r.get("label_literal", ""), r.get("cond", "")))
return rows, "\n".join(parts)
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="本体枚举器(游戏内容↔测试验证对账)")
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
entities = scan_tables() + scan_schemas() + scan_tscn_buttons() + scan_gui_input_classes() + scan_screens()
_rows, corpus = assertion_corpus()
categories: dict[str, dict] = {}
for ent in entities:
cat = categories.setdefault(ent["category"], {"total": 0, "matched": 0, "gaps": []})
cat["total"] += 1
if ent["id"] and ent["id"] in corpus:
cat["matched"] += 1
else:
cat["gaps"].append({"id": ent["id"], "id_field": ent.get("id_field", "")})
payload = {
"meta": {
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
"entities_total": len(entities),
"corpus_rows": len(_rows),
"honest_boundary": "grep 关联有假阳假阴——文本没带 id 的假缺口人工核销;交互命中归探针真输入登记",
},
"categories": [
{"category": k, "total": v["total"], "matched": v["matched"], "gaps": v["gaps"]}
for k, v in sorted(categories.items())
],
}
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
js = "/* 本体对账清单——coverage_enum.py 随跑重生成,人工核销走豁免清单(coverage_exemptions.js),不落本文件。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.ontology = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
with open(str(out), "w", encoding="utf-8", newline="\n") as fh:
fh.write(js)
gap_total = sum(len(c["gaps"]) for c in payload["categories"])
print("[ontology] 本体实体 %d / 断言文料行 %d / 缺口 %d(%d 类)→ %s" % (
len(entities), len(_rows), gap_total, len(payload["categories"]), out))
for c in payload["categories"]:
print(" %s:%d 实体 / %d 缺口" % (c["category"], c["total"], len(c["gaps"])))
return 0
if __name__ == "__main__":
raise SystemExit(main())