批2(QA harness·B覆盖总览面板+本体枚举器): 义务层=tools/extract_capability_list.py(README能力清单四列表按旅程段解析,7段103行,用例指针剥反引号); 执行层=tools/coverage_receipts.py六源回执明细化(冒烟/单元集成/系统/探针/渲染/静态各一.js,名×结果×计数粒度,日志缺失=not_run亮灰不冒充绿)+run_all_gates.sh末尾挂点(观测件非判定件,解析失败打红字不翻闸判)+probe_manifest.js(面板file://无法列目录,按现存登记文件落清单); 机器义务双源之本体源=tools/coverage_enum.py(两扫描器+比对:.tres行集/schema词表字段/tscn按钮/gui_input自绘类/屏模态清单×extract_checks断言文料,865实体/缺口212); 呈现层=tools/coverage_report.html(四节:六层闸况/能力行×六层灯按旅程段/控件下钻交互源+渲染golden展示源/本体对账缺口×探针命中降级探针承载;状态灯四态+数据时间戳陈旧可见+完备性边界页眉;JSONP直载file://即开); 豁免=tools/coverage_exemptions.js人工维护母版; 登记件扩seen面(场景树全量BaseButton+db布局表hotspot_key公共面枚举,seen-hit=存在但零真输入覆盖缺口数据源)。读数: 两探针窄跑绿(screen_flow 19命中/116存在,ui_click 27命中/91存在——停手钮/树购买重建代等真缺口在册,弃图刷新确认链走视图方法属衔接条目余项); 六源明细化对真实闸日志抽核(unit82套件/系统69检查/探针10段/静态13条/golden逐屏L1-L3); 面板浏览器真实入口四节渲染验过; 全量档闸【验收通过】无新增红(exit=0,回执commit 07d4977f,六源明细化在闸内实跑=挂点真路径验证)
This commit is contained in:
1 parent
07d4977f41
commit
bb9ce8a117
7 files changed
+745
No files matched your search
@@ -164,6 +164,38 @@ func _bump(key: String) -> void:
|
||||
_hits[key] = int(_hits.get(key, 0)) + 1
|
||||
|
||||
|
||||
# ---- 场景树全量交互面枚举(控件缺口数据源:seen−hit=「存在但零真输入覆盖」)----
|
||||
# 成员面与登记同源:场景树全部 BaseButton(main 建树常驻——未开过的屏的钮也算存在;
|
||||
# 已接线钮复用接线键防序位错位,未接线者落盘期才补键——落盘是终态动作,序位计数此后不再消费)
|
||||
# +营地物件(经 db 布局表 hotspot_key 公共数据面枚举;物件命中见 _on_camp_object 的 #物件id 后缀)。
|
||||
func enumerate_seen() -> Array[String]:
|
||||
var keys: Array[String] = []
|
||||
_seen_into(_main, keys)
|
||||
var db = _main.get("db")
|
||||
if db != null and db.get("presentation") != null:
|
||||
var layout = db.presentation.camp_scene_layout
|
||||
if layout != null:
|
||||
for spec in layout.layers:
|
||||
var hk: StringName = spec.hotspot_key
|
||||
var ck := "camp_object#" + String(hk)
|
||||
if hk != &"" and not keys.has(ck):
|
||||
keys.append(ck)
|
||||
return keys
|
||||
|
||||
|
||||
func _seen_into(node: Node, keys: Array[String]) -> void:
|
||||
if node is BaseButton:
|
||||
var id := node.get_instance_id()
|
||||
var key := String(_node_keys.get(id, ""))
|
||||
if key == "":
|
||||
key = _assign_key(node)
|
||||
_node_keys[id] = key
|
||||
if not keys.has(key):
|
||||
keys.append(key)
|
||||
for child in node.get_children():
|
||||
_seen_into(child, keys)
|
||||
|
||||
|
||||
# ---- 落盘(JSONP 式 .js——file:// 页面 <script src> 直载)----
|
||||
|
||||
func dump_js() -> void:
|
||||
@@ -183,6 +215,7 @@ func dump_js() -> void:
|
||||
var total := 0
|
||||
for k in _hits:
|
||||
total += int(_hits[k])
|
||||
var seen := enumerate_seen()
|
||||
var payload := {
|
||||
"meta": {
|
||||
"probe": _probe_name,
|
||||
@@ -192,9 +225,11 @@ func dump_js() -> void:
|
||||
"driver": "InputEvent via Viewport.push_input(真输入管线)",
|
||||
"key_shape": "锚链归一化(具名/脚本锚+类#文本+序位;@ 临时名不入键)",
|
||||
"hits_total": total,
|
||||
"seen_total": seen.size(),
|
||||
"skipped_temp_names": _skipped_temp,
|
||||
},
|
||||
"hits": _hits,
|
||||
"seen": seen,
|
||||
}
|
||||
var js := "/* 真输入覆盖登记——探针每次运行自动覆写,人工修订无效(豁免面走人工清单,不落本文件)。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/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)
|
||||
out.write_text(js, encoding="utf-8", newline="\n")
|
||||
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())
|
||||
@@ -0,0 +1,11 @@
|
||||
/* 覆盖豁免清单——人工维护(S13 基线人工修订同款纪律;面板与工具只读本文件,禁代码生成)。
|
||||
* 用途:登记「确认不需要真输入覆盖/不需要断言关联」的条目,B 面板对在册条目亮豁免灯、不亮缺口。
|
||||
* 形态:abilities=能力清单行「能力」列原文(精确匹配);controls=控件登记键或其中缀(包含匹配);
|
||||
* ontology=本体实体 id(精确匹配)。每条必须带 reason——无理由的豁免条目面板标红示警。 */
|
||||
window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};
|
||||
window.__QA_COVERAGE__.exemptions = {
|
||||
"meta": {"note": "人工维护;改后刷新面板即生效,无生成步骤"},
|
||||
"abilities": [],
|
||||
"controls": [],
|
||||
"ontology": []
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""coverage_receipts.py — 六源回执明细化(B 覆盖总览面板·执行层)
|
||||
|
||||
各闸跑批把当场日志落 `.gates/*.log`;本工具在跑批末尾被闸壳调用一次,把六层日志解析成
|
||||
「名×结果×计数」明细,按闸各写一个 JSONP .js 到 `.gates/coverage/`,供
|
||||
tools/coverage_report.html 经 <script src> 直载。
|
||||
|
||||
六层与日志源(层键=面板消费键,层名=呈现名):
|
||||
smoke 冒烟测试 smoke_boot.log 摘要「N 项检查,M 项失败」
|
||||
unit 单元与集成测试 unit.log 每套件「[run_all] PASS/FAIL res://…(exit=N)」
|
||||
system 系统测试 system_test.log 摘要「N 项检查,M 项失败」
|
||||
probe 场景探针 probe.log 每探针段「======== 名(段)========」+该段 PASS/FAIL 行
|
||||
render 渲染闸 render.log golden 清单与对比结果
|
||||
static 静态扫描 static_scan.log 每个 S 系条目「[static_scan] S# ok/FAIL(…)」
|
||||
|
||||
判据口径:
|
||||
- 明细粒度=「名×结果×计数」,不记逐条断言文本(契约 §4 成本口径);
|
||||
- 日志缺失=该层 {"result": "not_run"}——面板亮灰灯,不冒充绿;
|
||||
- 本工具失败不翻转闸判定(观测件非判定件),但必须打红字——静默丢数据等于断言蒸发。
|
||||
|
||||
用法: python tools/coverage_receipts.py [--stamp-dir <.gates 路径>]
|
||||
产出: <stamp-dir>/coverage/gate_<层键>.js × 6
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_STAMP = ROOT / "game" / "tests" / ".gates"
|
||||
|
||||
SUMMARY_RE = re.compile(r"摘要[::]\s*\[?[^\]]*\]?\s*摘要[::]\s*(\d+)\s*项检查,(\d+)\s*项失败")
|
||||
SUITE_RE = re.compile(r"\[run_all\] (PASS|FAIL) (res://\S+?\.gd)(?:\s*\(exit=(-?\d+)\))?")
|
||||
PROBE_SECTION_RE = re.compile(r"^======== (\S+)(?:\s+(.+?))? ========")
|
||||
PROBE_RESULT_RE = re.compile(r"^\[[^\]]+\] (PASS|FAIL)")
|
||||
GOLDEN_RE = re.compile(r"\[render_gate\] \[golden\] (PASS|FAIL)((\d+) 屏黄金图")
|
||||
GOLDEN_CROP_RE = re.compile(r"\[render_gate\] \[golden_crop\] (\S+?) —")
|
||||
STATIC_RE = re.compile(r"^\[static_scan\] (S\d+(?:/S\d+)*)\s+(ok|FAIL)", re.M)
|
||||
STATIC_GDSTYLE_RE = re.compile(r"^\[gdstyle_gate\] (ok|FAIL)", re.M)
|
||||
|
||||
|
||||
def read(stamp: Path, name: str) -> str:
|
||||
p = stamp / name
|
||||
if not p.is_file():
|
||||
return ""
|
||||
return p.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def parse_summary_layer(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
m = SUMMARY_RE.search(log)
|
||||
verdict = "绿" if "[run_smoke] PASS" in log or "[run_system_test] PASS" in log else (
|
||||
"红" if ("FAIL" in log or "摘要" in log and m and int(m.group(2)) > 0) else "灰")
|
||||
if m:
|
||||
return {"result": verdict, "checks": int(m.group(1)), "fails": int(m.group(2))}
|
||||
return {"result": verdict}
|
||||
|
||||
|
||||
def parse_unit(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items = [{"name": m.group(2), "result": m.group(1), "exit": int(m.group(3) or 0)}
|
||||
for m in SUITE_RE.finditer(log)]
|
||||
red = [i for i in items if i["result"] == "FAIL"]
|
||||
verdict = "绿" if "[run_all] all green" in log and not red else ("红" if red or "FAIL" in log else "灰")
|
||||
return {"result": verdict, "suites_total": len(items), "items": items}
|
||||
|
||||
|
||||
def parse_probe(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items: list[dict] = []
|
||||
cur: dict | None = None
|
||||
for line in log.splitlines():
|
||||
sm = PROBE_SECTION_RE.match(line.strip())
|
||||
if sm:
|
||||
seg = (sm.group(2) or "").strip()
|
||||
cur = {"name": sm.group(1), "segment": seg, "result": "灰"}
|
||||
items.append(cur)
|
||||
continue
|
||||
rm = PROBE_RESULT_RE.match(line.strip())
|
||||
if rm and cur is not None and cur["result"] == "灰":
|
||||
cur["result"] = rm.group(1)
|
||||
judged = [i for i in items if i["result"] != "灰"]
|
||||
verdict = "红" if (any(i["result"] == "FAIL" for i in judged) or "[probe_gate] 全绿" not in log) else "绿"
|
||||
return {"result": verdict, "probes_total": len(judged), "items": items}
|
||||
|
||||
|
||||
def parse_render(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items: list[dict] = []
|
||||
gm = GOLDEN_RE.search(log)
|
||||
if gm:
|
||||
items.append({"name": "golden 逐像素对比", "result": gm.group(1), "screens": int(gm.group(2))})
|
||||
screens: list[str] = []
|
||||
for m in GOLDEN_CROP_RE.finditer(log):
|
||||
if m.group(1) not in screens:
|
||||
screens.append(m.group(1))
|
||||
if screens:
|
||||
items.append({"name": "golden 逐屏裁剪", "result": "PASS", "screens": screens})
|
||||
verdict = "红" if "FAIL" in log or "[render_gate] 全绿" not in log else "绿"
|
||||
return {"result": verdict, "items": items}
|
||||
|
||||
|
||||
def parse_static(log: str) -> dict:
|
||||
if not log:
|
||||
return {"result": "not_run"}
|
||||
items = [{"name": m.group(1), "result": m.group(2)} for m in STATIC_RE.finditer(log)]
|
||||
gm = STATIC_GDSTYLE_RE.search(log)
|
||||
if gm:
|
||||
items.insert(0, {"name": "gdstyle", "result": gm.group(1)})
|
||||
red = [i for i in items if i["result"] != "ok"]
|
||||
verdict = "红" if red or "FAIL" in log else "绿"
|
||||
return {"result": verdict, "items_total": len(items), "items": items}
|
||||
|
||||
|
||||
LAYERS = [
|
||||
("smoke", "冒烟测试", "smoke_boot.log", parse_summary_layer),
|
||||
("unit", "单元与集成测试", "unit.log", parse_unit),
|
||||
("system", "系统测试", "system_test.log", parse_summary_layer),
|
||||
("probe", "场景探针", "probe.log", parse_probe),
|
||||
("render", "渲染闸", "render.log", parse_render),
|
||||
("static", "静态扫描", "static_scan.log", parse_static),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="六源回执明细化(B 面板·执行层)")
|
||||
ap.add_argument("--stamp-dir", default=str(DEFAULT_STAMP))
|
||||
args = ap.parse_args()
|
||||
stamp = Path(args.stamp_dir)
|
||||
out_dir = stamp / "coverage"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
failed = False
|
||||
wrote = 0
|
||||
for key, label, log_name, fn in LAYERS:
|
||||
try:
|
||||
detail = fn(read(stamp, log_name))
|
||||
detail["meta"] = {
|
||||
"layer": key,
|
||||
"label": label,
|
||||
"source_log": log_name,
|
||||
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
}
|
||||
payload = {"receipt": detail}
|
||||
js = "/* 闸回执明细化——coverage_receipts.py 随每次跑批覆写,人工修订无效。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.gate_receipts = window.__QA_COVERAGE__.gate_receipts || {};\n"
|
||||
js += "window.__QA_COVERAGE__.gate_receipts[%s] = %s;\n" % (
|
||||
json.dumps(key), json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
(out_dir / ("gate_%s.js" % key)).write_text(js, encoding="utf-8", newline="\n")
|
||||
wrote += 1
|
||||
print("[receipts] %s(%s)=%s" % (label, log_name, detail.get("result", "?")))
|
||||
except Exception as exc: # noqa: BLE001 —— 观测件逐层容错,红字可见不静默
|
||||
failed = True
|
||||
print("[receipts] 红:%s 层解析失败(%s):%r" % (label, log_name, exc))
|
||||
if wrote == 0:
|
||||
print("[receipts] 红:六层明细一件未落盘——禁静默")
|
||||
return 1
|
||||
# 探针登记文件清单(B 面板 file:// 无法列目录——按现存 probe_hits_*.js 落 manifest 供 <script src>)
|
||||
probe_files = sorted(p.name for p in out_dir.glob("probe_hits_*.js"))
|
||||
manifest = {
|
||||
"meta": {"generated_at": datetime.datetime.now().isoformat(timespec="seconds")},
|
||||
"probes": probe_files,
|
||||
}
|
||||
mjs = "/* 探针登记清单——coverage_receipts.py 随跑批按现存文件覆写,勿手编。 */\n"
|
||||
mjs += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
mjs += "window.__QA_COVERAGE__.probe_manifest = %s;\n" % json.dumps(manifest, ensure_ascii=False)
|
||||
(out_dir / "probe_manifest.js").write_text(mjs, encoding="utf-8", newline="\n")
|
||||
print("[receipts] 探针登记清单 %d 件 → probe_manifest.js" % len(probe_files))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,226 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>QA 覆盖总览面板——Dungeon Sweeper</title>
|
||||
<style>
|
||||
:root { --ok:#3a7d44; --bad:#a33; --off:#9aa; --na:#d5d5d0; --ex:#b8860b; --ink:#2c2a26; --paper:#f4f1ea; }
|
||||
body { font-family: "Microsoft YaHei", sans-serif; margin: 0; background: var(--paper); color: var(--ink); }
|
||||
header { padding: 14px 20px 10px; border-bottom: 3px solid var(--ink); background: #fff; }
|
||||
h1 { font-size: 19px; margin: 0 0 4px; }
|
||||
h2 { font-size: 16px; margin: 22px 0 6px; border-left: 5px solid var(--ink); padding-left: 8px; }
|
||||
.note { font-size: 12px; color: #555; margin: 3px 0; }
|
||||
.warn { color: var(--bad); font-weight: bold; }
|
||||
main { padding: 12px 20px 40px; }
|
||||
table { border-collapse: collapse; width: 100%; background: #fff; font-size: 12.5px; }
|
||||
th, td { border: 1px solid #cfc9bd; padding: 4px 7px; text-align: left; vertical-align: top; }
|
||||
th { background: #ebe6da; }
|
||||
.lamp { display: inline-block; width: 13px; height: 13px; border-radius: 50%; margin-right: 3px; vertical-align: -2px; }
|
||||
.ok { background: var(--ok); } .bad { background: var(--bad); }
|
||||
.off { background: var(--off); } .na { background: var(--na); } .ex { background: #fff; border: 3px double var(--ex); }
|
||||
.chip { display: inline-block; padding: 1px 8px; border-radius: 9px; color: #fff; font-size: 12px; }
|
||||
details { margin: 4px 0 10px; } summary { cursor: pointer; font-size: 13px; }
|
||||
.gaplist { font-size: 12px; color: #444; line-height: 1.7; }
|
||||
.seg { margin-bottom: 18px; }
|
||||
code { background: #eceade; padding: 0 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>QA 覆盖总览面板 <span style="font-weight:normal;font-size:13px">(义务 × 执行合并视图——只读)</span></h1>
|
||||
<div class="note">状态灯:绿=该层最近跑批过 红=跑批挂 灰=该层无承载/未跑 双圈=豁免(<code>tools/coverage_exemptions.js</code> 人工维护)。</div>
|
||||
<div class="note" id="staleness"></div>
|
||||
<div class="note"><b>完备性边界</b>:控件级视图只对「被探针/C 打开过的屏」负责;未打开过的屏由动线断言+C 探索兜底。
|
||||
本体对账为文本 grep 关联——有假阳假阴(断言测了实体但文本没带 id=假缺口,人工核销;文本带 id 但断言弱=假覆盖),缺口须人工复核后再立事项。</div>
|
||||
<div class="note">数据源:随最近一次闸批落盘于 <code>game/tests/.gates/coverage/</code>;刷新本页即读最新。</div>
|
||||
</header>
|
||||
<main>
|
||||
<h2>一、六层闸况</h2>
|
||||
<div id="gateSummary"></div>
|
||||
|
||||
<h2>二、能力清单 × 六层状态灯(按旅程段)</h2>
|
||||
<div class="note">灯的判据:用例文件落哪层就钉哪层的回执(单元=逐套件 PASS/FAIL;探针/系统/冒烟/渲染/静态=层判据);该层无对应承载=灰。</div>
|
||||
<div id="capabilityMatrix"></div>
|
||||
|
||||
<h2>三、控件级交互覆盖下钻(探针真输入登记)</h2>
|
||||
<div class="note">交互源=探针真输入命中登记(push_input 管线命中);「存在但零命中」=控件缺口——hitbox 错位/遮挡/驱动空洞在此暴露。展示源=渲染闸 golden 逐屏登记。</div>
|
||||
<div id="controlDrill"></div>
|
||||
|
||||
<h2>四、本体对账(游戏内容 ↔ 测试验证)</h2>
|
||||
<div class="note">缺口=本体存在、断言文料零关联;若该 id 出现在探针命中登记则降级为「探针承载」。首见批次=本工具生成时间,逐批对比看新面孔。</div>
|
||||
<div id="ontologyGaps"></div>
|
||||
</main>
|
||||
<script src="./coverage_exemptions.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/capability_list.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_smoke.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_unit.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_system.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_probe.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_render.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/gate_static.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/probe_manifest.js"></script>
|
||||
<script src="../game/tests/.gates/coverage/ontology_gaps.js"></script>
|
||||
<script>
|
||||
/* 探针登记文件由 manifest 同步补挂(document.write 在解析期阻塞加载,渲染段前数据已就位) */
|
||||
(function () {
|
||||
const probes = ((window.__QA_COVERAGE__ || {}).probe_manifest || {}).probes || [];
|
||||
for (const p of probes) {
|
||||
document.write('<script src="../game/tests/.gates/coverage/' + encodeURIComponent(p) + '"><\/script>');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
"use strict";
|
||||
const C = window.__QA_COVERAGE__ || {};
|
||||
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); }
|
||||
function lamp(cls, title) { return `<span class="lamp ${cls}" title="${esc(title)}"></span>`; }
|
||||
function chip(txt, cls) { return `<span class="chip" style="background:${cls}">${esc(txt)}</span>`; }
|
||||
const VCOLOR = {"绿":"var(--ok)","红":"var(--bad)","灰":"var(--off)","not_run":"var(--off)"};
|
||||
|
||||
// ── 数据新鲜度(陈旧可见——防静默旧数据) ──
|
||||
(function () {
|
||||
const stamps = [];
|
||||
const cl = (C.capability_list || {}).meta; if (cl) stamps.push(`义务层 ${cl.generated_at}`);
|
||||
const rc = C.gate_receipts || {};
|
||||
for (const k in rc) { const m = (rc[k].receipt || {}).meta; if (m) stamps.push(`${m.label} ${m.generated_at}`); }
|
||||
const on = (C.ontology || {}).meta; if (on) stamps.push(`本体 ${on.generated_at}`);
|
||||
const pm = (C.probe_manifest || {}).meta; if (pm) stamps.push(`探针登记 ${pm.generated_at}`);
|
||||
document.getElementById("staleness").innerHTML = "数据时间:" + stamps.map(esc).join(" · ");
|
||||
})();
|
||||
|
||||
// ── 一、六层闸况 ──
|
||||
(function () {
|
||||
const rc = C.gate_receipts || {};
|
||||
const ORDER = ["smoke", "unit", "system", "probe", "render", "static"];
|
||||
const LABEL = {smoke:"冒烟测试", unit:"单元与集成测试", system:"系统测试", probe:"场景探针", render:"渲染闸", static:"静态扫描"};
|
||||
let html = "<table><tr><th>层</th><th>判定</th><th>明细</th></tr>";
|
||||
for (const k of ORDER) {
|
||||
const r = (rc[k] || {}).receipt;
|
||||
if (!r) { html += `<tr><td>${LABEL[k]}</td><td>${chip("缺数据", "var(--off)")}</td><td>—</td></tr>`; continue; }
|
||||
const v = r.result === "not_run" ? "未跑" : r.result;
|
||||
const color = VCOLOR[r.result] || "var(--off)";
|
||||
let detail = `共 ${r.items ? r.items.length : 0} 项`;
|
||||
if (r.checks != null) detail = `${r.checks} 项检查 / ${r.fails} 失败`;
|
||||
if (r.suites_total != null) detail = `${r.suites_total} 套件`;
|
||||
if (r.probes_total != null) detail = `${r.probes_total} 探针`;
|
||||
html += `<tr><td>${LABEL[k]}</td><td>${chip(v, color)}</td><td>${esc(detail)} <span class="note">源=${esc(r.meta.source_log)} @ ${esc(r.meta.generated_at)}</span></td></tr>`;
|
||||
}
|
||||
html += "</table><details><summary>单元与集成逐套件明细</summary><div class='gaplist'>";
|
||||
const unit = ((rc.unit || {}).receipt || {}).items || [];
|
||||
for (const it of unit) {
|
||||
html += `${lamp(it.result === "PASS" ? "ok" : "bad", it.result)} ${esc(it.name.replace("res://tests/", ""))}${it.exit ? "(exit=" + esc(it.exit) + ")" : ""}<br>`;
|
||||
}
|
||||
html += "</div></details><details><summary>场景探针逐段明细</summary><div class='gaplist'>";
|
||||
const pr = ((rc.probe || {}).receipt || {}).items || [];
|
||||
for (const it of pr) {
|
||||
html += `${lamp(it.result === "PASS" ? "ok" : (it.result === "FAIL" ? "bad" : "off"), it.result)} ${esc(it.name)}${it.segment ? "(" + esc(it.segment) + ")" : ""}<br>`;
|
||||
}
|
||||
html += "</div></details><details><summary>静态扫描逐条明细</summary><div class='gaplist'>";
|
||||
const st = ((rc.static || {}).receipt || {}).items || [];
|
||||
for (const it of st) {
|
||||
html += `${lamp(it.result === "ok" ? "ok" : "bad", it.result)} ${esc(it.name)}<br>`;
|
||||
}
|
||||
html += "</div></details>";
|
||||
document.getElementById("gateSummary").innerHTML = html;
|
||||
})();
|
||||
|
||||
// ── 二、能力清单 × 六层灯 ──
|
||||
(function () {
|
||||
const cl = C.capability_list;
|
||||
if (!cl || !cl.segments) { document.getElementById("capabilityMatrix").innerHTML = "<p class='warn'>义务层缺数据——先跑 tools/extract_capability_list.py</p>"; return; }
|
||||
const rc = C.gate_receipts || {};
|
||||
const ex = ((C.exemptions || {}).abilities || []).map(s => String(s));
|
||||
function layerOf(caseFile) {
|
||||
if (/system_test/.test(caseFile)) return "system";
|
||||
if (/probe\//.test(caseFile) || /_probe\.gd/.test(caseFile)) return "probe";
|
||||
if (/golden|render/.test(caseFile)) return "render";
|
||||
return "unit";
|
||||
}
|
||||
function rowLight(row) {
|
||||
if (ex.includes(row.ability)) return lamp("ex", "豁免(coverage_exemptions.js 在册)");
|
||||
if (!row.cases || !row.cases.length) return lamp("off", "无代表用例指针");
|
||||
let worst = null; const seenLayers = new Set();
|
||||
for (const c of row.cases) {
|
||||
const layer = layerOf(c);
|
||||
seenLayers.add(layer);
|
||||
const r = (rc[layer] || {}).receipt;
|
||||
if (!r || r.result === "not_run") { worst = worst || "na"; continue; }
|
||||
if (r.result === "红") return lamp("bad", `${layer} 层跑批挂`);
|
||||
if (layer === "unit") {
|
||||
const items = r.items || [];
|
||||
const hit = items.filter(it => c.indexOf(it.name.replace("res://tests/", "")) >= 0 || it.name.indexOf(c.split(":")[0].replace(".gd", "")) >= 0);
|
||||
if (!hit.length) { worst = worst || "off"; continue; }
|
||||
if (hit.some(it => it.result !== "PASS")) return lamp("bad", "代表套件红:" + c);
|
||||
worst = worst || "ok";
|
||||
} else {
|
||||
worst = worst || (r.result === "绿" ? "ok" : "off");
|
||||
}
|
||||
}
|
||||
if (worst === "ok") return lamp("ok", "承载层绿");
|
||||
if (worst === "na") return lamp("na", "承载层本批未跑");
|
||||
return lamp("off", "该层无对应承载(灰)");
|
||||
}
|
||||
let html = "";
|
||||
for (const seg of cl.segments) {
|
||||
if (!seg.rows.length) continue;
|
||||
html += `<div class="seg"><h3 style="font-size:14px;margin:8px 0 4px">${esc(seg.segment)}(${seg.rows.length} 行)</h3>`;
|
||||
html += "<table><tr><th style='width:32%'>能力</th><th>系统</th><th style='width:14%'>灯</th><th style='width:26%'>代表用例</th></tr>";
|
||||
for (const row of seg.rows) {
|
||||
html += `<tr><td>${esc(row.ability)}</td><td>${esc(row.system)}</td><td>${rowLight(row)}</td><td>${row.cases.map(esc).join("<br>")}</td></tr>`;
|
||||
}
|
||||
html += "</table></div>";
|
||||
}
|
||||
document.getElementById("capabilityMatrix").innerHTML = html;
|
||||
})();
|
||||
|
||||
// ── 三、控件级下钻 ──
|
||||
(function () {
|
||||
const pm = C.probe_manifest || {probes: []};
|
||||
const ex = ((C.exemptions || {}).controls || []).map(s => String(s));
|
||||
const rd = ((C.gate_receipts || {}).render || {}).receipt || {};
|
||||
const renderScreens = [];
|
||||
for (const it of (rd.items || [])) if (it.screens instanceof Array) renderScreens.push(...it.screens);
|
||||
let html = "";
|
||||
if (renderScreens.length) {
|
||||
html += `<p class="note"><b>渲染展示登记(golden 逐屏)</b>:${renderScreens.map(esc).join("、")}</p>`;
|
||||
}
|
||||
for (const pf of pm.probes) {
|
||||
const name = pf.replace(/^probe_hits_/, "").replace(/\.js$/, "");
|
||||
// 同名重复 <script> 无法动态注入——manifest 落盘时同步内联一份数据入口由面板经 window 取
|
||||
const data = (window.__QA_COVERAGE__.probe_real_input || {})[name];
|
||||
if (!data) { html += `<p class="warn">${esc(name)}:登记文件未加载(检查 script 标签与 manifest)</p>`; continue; }
|
||||
const hits = data.hits || {};
|
||||
const seen = data.seen || [];
|
||||
const gaps = seen.filter(k => !(k in hits) && !ex.some(e => k.indexOf(e) >= 0));
|
||||
html += `<details open><summary><b>${esc(name)}</b>:命中 ${Object.keys(hits).length} 控件 / 存在 ${seen.length} 控件 / 零命中缺口 ${gaps.length}(@ ${esc(data.meta.generated_at)})</summary><div class='gaplist'>`;
|
||||
for (const g of gaps) html += `${lamp("off", "存在但零真输入命中")} ${esc(g)}<br>`;
|
||||
html += "<b>已命中:</b><br>";
|
||||
for (const k of Object.keys(hits).sort()) html += `${lamp("ok", "真输入命中 " + hits[k] + " 次")} ${esc(k)}×${hits[k]}<br>`;
|
||||
html += "</div></details>";
|
||||
}
|
||||
document.getElementById("controlDrill").innerHTML = html || "<p class='warn'>探针登记缺数据——先跑探针与 tools/coverage_receipts.py</p>";
|
||||
})();
|
||||
|
||||
// ── 四、本体对账 ──
|
||||
(function () {
|
||||
const on = C.ontology;
|
||||
if (!on || !on.categories) { document.getElementById("ontologyGaps").innerHTML = "<p class='warn'>本体数据缺——先跑 tools/coverage_enum.py</p>"; return; }
|
||||
const ex = ((C.exemptions || {}).ontology || []).map(s => String(s));
|
||||
const hitTexts = [];
|
||||
const pri = (C.probe_real_input || {});
|
||||
for (const k in pri) for (const hk of Object.keys(pri[k].hits || {})) hitTexts.push(hk);
|
||||
let html = `<p class="note">实体 ${esc(on.meta.entities_total)} · 断言文料 ${esc(on.meta.corpus_rows)} 行 · 首见批次 ${esc(on.meta.generated_at)}</p>`;
|
||||
for (const cat of on.categories) {
|
||||
const shown = cat.gaps.filter(g => !ex.includes(g.id));
|
||||
const probed = shown.filter(g => hitTexts.some(h => h.indexOf(g.id) >= 0));
|
||||
const pure = shown.filter(g => !probed.includes(g));
|
||||
html += `<details><summary><b>${esc(cat.category)}</b>:${cat.total} 实体 / 关联 ${cat.matched} / 缺口 ${shown.length}(其中探针承载 ${probed.length})</summary><div class='gaplist'>`;
|
||||
for (const g of probed) html += `${lamp("ok", "探针真输入命中承载")} ${esc(g.id)} <span class="note">(断言文料零关联——交互覆盖由探针承载)</span><br>`;
|
||||
for (const g of pure) html += `${lamp("off", "验证零关联缺口——人工复核后再立事项")} ${esc(g.id)}<br>`;
|
||||
html += "</div></details>";
|
||||
}
|
||||
document.getElementById("ontologyGaps").innerHTML = html;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""extract_capability_list.py — 能力清单义务层提取器(B 覆盖总览面板·义务层)
|
||||
|
||||
把 game/tests/README.md「能力清单」的四列表(能力|系统|真实入口|代表用例)按旅程段
|
||||
解析成机器可读 JSONP .js,供 tools/coverage_report.html 经 <script src> 直载
|
||||
(file:// 下 fetch 本地 json 被 CORS 拦——JSONP 是 B 面板全数据源的统一形态)。
|
||||
|
||||
用法:
|
||||
python tools/extract_capability_list.py [--readme <路径>] [--out <路径>]
|
||||
|
||||
判据口径:
|
||||
- 只认表头恰为「能力|系统|真实入口|代表用例」的四列表(覆盖域表等五列表不取);
|
||||
- 旅程段标题形态=「**旅程段N:名称**」,段名进每行的 segment 字段;
|
||||
- 代表用例列按「;」切分成用例指针数组(`文件:函数` 形态逐字保真);
|
||||
- 本工具是义务层的静态投影,产出随 README 变更重跑即得——面板只读本产物,禁在面板里改义务。
|
||||
|
||||
产出(JSONP):
|
||||
window.__QA_COVERAGE__.capability_list = {
|
||||
"meta": {"generated_at", "source", "rows_total", "segments_total"},
|
||||
"segments": [{"segment": 名称, "rows": [{"ability", "system", "entry", "cases": []}]}],
|
||||
}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_README = ROOT / "game" / "tests" / "README.md"
|
||||
DEFAULT_OUT = ROOT / "game" / "tests" / ".gates" / "coverage" / "capability_list.js"
|
||||
|
||||
SEGMENT_RE = re.compile(r"^\*\*旅程段[一二三四五六七八九十]+[::](.+?)\*\*\s*$")
|
||||
|
||||
|
||||
|
||||
def split_row(line: str) -> list[str]:
|
||||
body = line.strip()
|
||||
if body.startswith("|"):
|
||||
body = body[1:]
|
||||
if body.endswith("|"):
|
||||
body = body[:-1]
|
||||
return [c.strip() for c in body.split("|")]
|
||||
|
||||
|
||||
def parse(readme: Path) -> dict:
|
||||
lines = readme.read_text(encoding="utf-8").splitlines()
|
||||
segments: list[dict] = []
|
||||
cur_seg: str | None = None
|
||||
cur_rows: list[dict] | None = None
|
||||
in_capability_table = False
|
||||
for line in lines:
|
||||
seg = SEGMENT_RE.match(line.strip())
|
||||
if seg:
|
||||
if cur_seg is not None:
|
||||
segments.append({"segment": cur_seg, "rows": cur_rows or []})
|
||||
cur_seg = seg.group(1).strip()
|
||||
cur_rows = []
|
||||
in_capability_table = False
|
||||
continue
|
||||
if cur_seg is None:
|
||||
continue
|
||||
if not line.strip().startswith("|"):
|
||||
in_capability_table = False
|
||||
continue
|
||||
cells = split_row(line)
|
||||
if all(not set(c) - {"-", " ", ":"} for c in cells) and cells:
|
||||
continue # 表头分隔行 |---|---|
|
||||
if (len(cells) == 4 and cells[0] == "能力" and cells[1] == "系统"
|
||||
and cells[2] == "真实入口" and cells[3].startswith("代表用例")):
|
||||
in_capability_table = True
|
||||
continue
|
||||
if in_capability_table and len(cells) == 4:
|
||||
ability, system, entry, cases = cells
|
||||
cur_rows.append({
|
||||
"ability": ability.strip("`"),
|
||||
"system": system,
|
||||
"entry": entry,
|
||||
"cases": [c.strip().strip("`") for c in cases.split(";") if c.strip()],
|
||||
})
|
||||
if cur_seg is not None:
|
||||
segments.append({"segment": cur_seg, "rows": cur_rows or []})
|
||||
rows_total = sum(len(s["rows"]) for s in segments)
|
||||
return {
|
||||
"meta": {
|
||||
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"source": str(readme),
|
||||
"rows_total": rows_total,
|
||||
"segments_total": len(segments),
|
||||
},
|
||||
"segments": segments,
|
||||
}
|
||||
|
||||
|
||||
def dump_js(payload: dict, out: Path) -> None:
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
js = "/* 能力清单义务层投影——由 extract_capability_list.py 生成,随 README 变更重跑;人工修订无效(义务源=README 能力清单)。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.capability_list = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
out.write_text(js, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="能力清单义务层提取(B 面板·义务层)")
|
||||
ap.add_argument("--readme", default=str(DEFAULT_README))
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
readme = Path(args.readme)
|
||||
if not readme.is_file():
|
||||
print("[capability] README 不存在:%s" % readme)
|
||||
return 1
|
||||
payload = parse(readme)
|
||||
dump_js(payload, Path(args.out))
|
||||
print("[capability] 义务层投影已落盘 %s(旅程段 %d / 能力行 %d)" % (
|
||||
args.out, payload["meta"]["segments_total"], payload["meta"]["rows_total"]))
|
||||
if payload["meta"]["rows_total"] == 0:
|
||||
print("[capability] 红:能力行数为 0——表头或旅程段标题形态不匹配,禁当空义务面用")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -648,5 +648,7 @@ RC=$?
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════"
|
||||
printf ' 回执:%s\n 基线:%s\n commit:%s\n' "$RECEIPT" "$BASELINE" "$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '?')"
|
||||
# B 覆盖总览面板·执行层六源明细化(观测件非判定件——解析失败打红字不翻闸判,coverage_receipts.py 头注)
|
||||
"$PY" "$REPO/tools/coverage_receipts.py" --stamp-dir "$STAMP_DIR" || true
|
||||
echo "════════════════════════════════════════════"
|
||||
exit $RC
|
||||
Reference in new issue
Block a user