chore(工具,文档): 台架 rc 语义与导航硬闸用例、门禁环境诊断、护栏还债算法、mem-budget 跨树口径

四条道的收尾件同笔(全是 python 工具与文档,零固件源;各段归属按道署明)。

· **台架(bench-runner)**:verify_log.py 新增 classify_measurement() 与 measureme
nt.* 字段,零行/零锚点
  判 exit 2 并打 [MEASUREMENT-BROKEN],**但同帧有 Forbidden 命中或自检故障仍判 1**(专门造了「零锚点 +
  [ERR] MEMFAULT」的陷阱日志实测——崩溃不许被降级成「没数据」);--selftest 16 → 22 条。board_ui_nav.py
  把两条执行页安全闸做成 7 条常驻用例接进提交钩子跑的快检档,作者把自己用例**证伪三轮**(测试强制上限、
  清单清空时是崩不是判定、出厂默认被翻允许也照样绿)后三条危险改动逐条能红。台架卫生清单落 §7:每圈复位
  前 open + 断言 ready + 看 rx 增长(今晚 daemon 死了 status 自愈把它遮住)、**Forbidden 令牌不得落在
  英文单词内部**(裸 Fault 命中 default 两次假红)、改判定语义前先 grep 消费者。
· **环境(ui-param-writer)**:证伪卡片前提两处——identify 那个版本根本没有 tags 模块(RECORD 与磁盘逐项
  一致,pre-commit 自己也从 identify.identify 取 ALL_TAGS,全仓零引用),真问题是 44 条 system 钩子里
  42 条 entry 用裸 python → PATH 首位那个零第三方包解释器,今天没炸纯靠两件事同时成立。交付是**新增 entry
  硬规则 + 修复死顺序与回滚并标注等静默窗口**,没有动共享环境(此刻重装会让别人的钩子在半途换解释器)。
· **门禁(gate-docs-fixer 已入库的 51 号钩子)**:本篇带上 host-unit-tests 的裁定句落点说明。
· **内存(comm-config-writer)**:mem-budget skill 补跨树测量身份口径(脚本按 __file__ 定位产物、什么才算
  一次新测量、多链同涨同一数值的共享帧/幻影环归因法)+ 看门狗说明的指针行。

实测:verify_log --selftest 22/22、board_ui_nav --selftest-quick rc=0(硬闸 7/7)、
doc-links --all exit 0、
check_ids --strict exit 0 且绝对数 1,156 **未被本笔吃掉**;全部文件 LF / 无 BOM / U+FFFD=0
;本笔零固件源,
随上一批权威构建验证过(0 错 0 警),故不再单独 @build。
This commit is contained in:
2026-10-27 05:16:58 +08:00
parent f1d0b5c2d9
commit d286b69a2d
10 changed files with 499 additions and 30 deletions
+143 -7
View File
@@ -841,6 +841,136 @@ BOARD_TRUTH = {
}
def selftest_safety_gates(clf):
"""两条执行风险硬闸的常驻回归用例(假设备、内存帧、零真机注键)。
为什么要常驻这两条:闸只在真出事的那一屏才起作用,平时没人碰,于是"改了个常量
就把闸拆掉"这种回退不会有任何人察觉。本用例不连 7003、不开串口、不碰台架——
设备是一帧内存里的空白位图,按键只记账、不投递。
覆盖的两条闸(2026-10-26 台架叠险 → ac9ca33f 收口,理由见文件头):
闸① UNKNOWN 退避里的 ENTER 次数上限(BACKOFF_MAX_E):需要第二次就当场停机;
以及显式改用无 ENTER 键序(--backoff-keys sl)时,一个 ENTER 都不许投出去;
闸② 执行类页面(EXEC_PAGE_IDS)默认拒绝下钻,且拒绝发生在**任何键投出去之前**;
显式放行(--allow-exec-pages)后不得再报"刻意跳过"——放行 = 接受后果。
返回 (用例数, 失败列表),与其它 selftest_* 同形,失败非 0 退出由调用方决定。
"""
g = globals()
saved = {"classify": g["classify"], "ALLOW_EXEC_PAGES": g["ALLOW_EXEC_PAGES"],
"BACKOFF_KEYS": g["BACKOFF_KEYS"], "BACKOFF_MAX_E": g["BACKOFF_MAX_E"]}
bad = []
n = 0
class FakeDevice(object):
"""假设备:grab 永远给同一帧内存位图,key 只记账不投递(不连板、不连模拟器)。"""
def __init__(self, fr):
self.fr = fr
self.keys = []
def grab(self):
return self.fr
def key(self, ch):
if ch not in KEYS:
raise ValueError("未知键 " + repr(ch))
self.keys.append(ch)
return self.fr
def stub(kind, extra=None):
def _f(fr, _clf):
st = {"kind": kind, "page": None, "cols": None, "evid": 0, "no": None}
if extra:
st.update(extra)
return st
return _f
def check(name, ok, detail):
print("%-5s %s %s" % ("[OK]" if ok else "[FAIL]", name, detail))
if not ok:
bad.append(name + ": " + detail)
blank = Frame(bytes(FRAME_BYTES), "fake_blank")
try:
# 政策钉:出厂默认必须是"拒绝下钻"。下面所有用例会临时改这两个常量,
# 所以默认值只能在进函数时抓一次(saved)——否则有人把默认翻成 True,
# 用例照样自己设 False 再测,全绿放行一个危险的默认(实测漏过一次)。
n += 1
check("闸② 出厂默认即拒绝(ALLOW_EXEC_PAGES 默认 False)",
saved["ALLOW_EXEC_PAGES"] is False,
"模块默认=%r(True 意味着任何人漏写 --allow-exec-pages 也会下钻执行类页面)"
% (saved["ALLOW_EXEC_PAGES"],))
# ---- 闸①:UNKNOWN 退避最多投 BACKOFF_MAX_E 次 ENTER,第二次当场停机 ----
g["classify"] = stub("UNKNOWN")
g["BACKOFF_KEYS"] = "esl"
# 故意**不**覆盖 BACKOFF_MAX_E:闸的强度本身要被钉住(有人把上限调到 2 就该红)
dev = FakeDevice(blank)
msg1 = ""
try:
Navigator(dev, clf).to_root(limit=12)
except RuntimeError as exc:
msg1 = str(exc)
n += 1
e_cnt = dev.keys.count("e")
check("闸① UNKNOWN 退避 ENTER 上限",
("BACKOFF_MAX_E" in msg1) and e_cnt == saved["BACKOFF_MAX_E"]
and saved["BACKOFF_MAX_E"] <= 1,
"停机消息=%s,投出 ENTER=%d 次(上限配置=%d,政策钉死 <=1),键迹=%s"
% (bool(msg1), e_cnt, saved["BACKOFF_MAX_E"], "".join(dev.keys)))
# ---- 闸①补:显式无 ENTER 键序时,一个 ENTER 都不许出现 ----
g["BACKOFF_KEYS"] = "sl"
dev2 = FakeDevice(blank)
try:
Navigator(dev2, clf).to_root(limit=6)
except RuntimeError:
pass
n += 1
check("闸①补 无 ENTER 键序(--backoff-keys sl)", "e" not in dev2.keys,
"键迹=%s(应不含 e)" % ("".join(dev2.keys) or "-"))
# ---- 闸②:执行类页面默认拒绝,且拒绝发生在投键之前 ----
g["BACKOFF_KEYS"] = "esl"
g["ALLOW_EXEC_PAGES"] = False
g["classify"] = stub("MENU", {"cols": [("根", 0)]})
# 清单被清空 = 闸被拆。这种情况要**报红**,不能"零条用例"悄悄过去,也不能崩
# (崩了只是噪声,红色才是判定;ng==0 只能兜住"一条都没跑",兜不住"清单空了")
n += 1
check("闸② 执行类清单完整(清空或缺项 = 拆闸)",
len(EXEC_PAGE_IDS) >= 2 and {"clear", "reset"} <= set(EXEC_PAGE_IDS),
"EXEC_PAGE_IDS=%s" % (sorted(EXEC_PAGE_IDS) or "空"))
for pid in sorted(set(EXEC_PAGE_IDS) | {"clear", "reset"}):
dev3 = FakeDevice(blank)
msg3 = ""
try:
Navigator(dev3, clf).go_page(pid)
except RuntimeError as exc:
msg3 = str(exc)
n += 1
check("闸② 执行类页面 %s 默认拒绝下钻" % pid,
("刻意跳过执行类页面" in msg3) and ("真执行" in msg3)
and ("e" not in dev3.keys),
"拒绝理由点名真执行=%s,投出的 ENTER=%d(消息:%s)"
% ("真执行" in msg3, dev3.keys.count("e"), msg3[:36] or "没抛错"))
# ---- 闸②反向:显式放行后不得再报"刻意跳过"(放行 = 接受后果)----
g["ALLOW_EXEC_PAGES"] = True
g["classify"] = stub("UNKNOWN") # 放行后走进导航,被闸①停住而不是被闸②拒绝
dev4 = FakeDevice(blank)
msg4 = ""
try:
Navigator(dev4, clf).go_page(sorted(set(EXEC_PAGE_IDS) | {"clear", "reset"})[0])
except RuntimeError as exc:
msg4 = str(exc)
n += 1
check("闸②反向 --allow-exec-pages 放行后不再拒绝",
("刻意跳过执行类页面" not in msg4) and bool(msg4),
"放行者=%s(消息:%s)" % ("刻意跳过执行类页面" not in msg4, msg4[:36] or "没抛错"))
return n, bad
finally:
for k, v in saved.items():
g[k] = v
def selftest_board_frames(clf):
"""真机留档帧上的分类自检:不需要台架也能证明认屏认得对。"""
bad = []
@@ -1032,13 +1162,17 @@ def main(argv):
return 0
if "--selftest-quick" in argv:
# 提交门禁用的快检档:只核"认屏认得对",不需要模拟器构建产物、不需要台架
print("[1/2] 仓库黄金参考分类(模拟器与板端同一份 UI 源码 = 语义基线)")
print("[1/3] 仓库黄金参考分类(模拟器与板端同一份 UI 源码 = 语义基线)")
nr, badr = selftest_refs(clf)
print("[2/2] 真机留档帧分类(目录缺则整段跳过,不算失败)")
print("[2/3] 真机留档帧分类(目录缺则整段跳过,不算失败)")
n, bad1 = selftest_board_frames(clf)
print("汇总(快检):黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d" % (nr, len(badr), n, len(bad1)))
print("[3/3] 执行风险硬闸(假设备、零真机注键,task-41)")
ng, badg = selftest_safety_gates(clf)
print("汇总(快检):黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d;硬闸用例 %d 条错 %d"
% (nr, len(badr), n, len(bad1), ng, len(badg)))
print("闭环导航未跑(需 lvgl_sim.exe):完整自检用 --selftest")
return 1 if (badr or bad1 or nr == 0) else 0
# 硬闸用例一条没跑也算失败:静默跳过等于把闸拆了还显示绿(同 nr==0 的处理口径)
return 1 if (badr or bad1 or badg or nr == 0 or ng == 0) else 0
if "--selftest" in argv:
print("[1/3] 仓库黄金参考分类(模拟器与板端同一份 UI 源码 = 语义基线)")
nr, badr = selftest_refs(clf)
@@ -1050,11 +1184,13 @@ def main(argv):
all_ids = [p for (_l, _n, p, _np) in clf.tree if p]
m, bad2, log = selftest_closed_loop(clf, all_ids)
_dump_trace(log)
print("汇总:黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d;闭环 %d 页失败 %d"
% (nr, len(badr), n, len(bad1), m, len(bad2)))
print("[4/4] 执行风险硬闸(假设备、零真机注键,task-41)")
ng, badg = selftest_safety_gates(clf)
print("汇总:黄金参考 %d 帧错 %d;留档帧 %d 帧错 %d;闭环 %d 页失败 %d;硬闸用例 %d 条错 %d"
% (nr, len(badr), n, len(bad1), m, len(bad2), ng, len(badg)))
# 留档帧目录(ci-artifacts/uiwalk,不受版本控制)可以不存在 → 那一段不算失败;
# 但黄金参考与闭环必须真跑过:任一为 0 说明这次"自检"其实什么都没检查
return 1 if (badr or bad1 or bad2 or nr == 0 or m == 0) else 0
return 1 if (badr or bad1 or bad2 or badg or nr == 0 or m == 0 or ng == 0) else 0
if "--walk" in argv:
rc = bench_gate(argv, "--walk")
if rc:
+5
View File
@@ -71,6 +71,11 @@ def _global_python_candidates():
L0 全灭就是 workflow 把候选收窄成 PATH python + 仓库 .venv(runner 上两者
都是死路),而真正装了 pre-commit 的机器级解释器(如
C:\\ProgramData\\anaconda3\\python.exe)不再被探测。
本机实测矩阵(哪个解释器装了哪些包、钩子实际跑在谁身上)见
docs/环境与部署/本地工具路径环境变量方案.md §5:提交期钩子由 .git/hooks/pre-commit
里的 INSTALL_PYTHON 钉死在仓库 .venv,而 language: system 的 entry 用裸 python 时
**不继承**它——所以依赖第三方包的脚本必须像本函数这样自己解析解释器。
"""
out = []
pf = os.environ.get("ProgramFiles", r"C:\Program Files")