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
@@ -16,10 +16,15 @@
[--require-order 'App init OK>Network Init>net init: OK']
[--done-marker 'net init: OK'] [--max-boot-ms 8000] [--json]
退出码(与旧版一致):
退出码(2026-10-27 task-32 起把 2 讲清楚;@test 那层的码不变):
0 = PASS(所有 Required 标记找到,Forbidden 零命中,汇总行要求满足)
1 = FAIL(缺 Required / 命中 Forbidden / 汇总行要求不满足)
2 = ERROR(日志文件不存在/不可读)
1 = FAIL(**设备侧真有坏消息**:缺 Required / 命中 Forbidden / 汇总行不满足 / 顺序或耗时超限)
2 = **测量没成立**(日志不存在或不可读;或开了 --max-boot-count 却零启动锚点 = 采集器没开、
日志被清、或采集没从复位开始)——这**不是**一次设备故障,按码聚合的脚本必须把它和 1
分开算,否则"采集断了"会被统计成"又崩了一次"。verdict 字段给 MEASUREMENT_BROKEN,
JSON 里 measurement.* 带三计数明细(payload_lines / boot_anchor_count / exact_hits /
loose_neighbours)。注意优先级:**零锚点但同时有 Forbidden 命中或自检故障记录时仍判 1**,
测量问题最多在旁边加一行 WARN——绝不能把真崩溃降级成"没数据"。
默认 Required 标记对应 STM32F4-Base 启动序列(见 App/app/app_main.c + App/task/net_task.c),
按测试用例覆盖,例如:
@@ -224,6 +229,31 @@ def _reset_cause_of(line, marker):
return tail[:j] if j > 0 else tail
# --- measurement-vs-device classification (task-32; bench-runner night rule) ---
# Three counters decide which kind of "nothing found" we are looking at, and they must not
# be conflated: an all-zero read is a BROKEN MEASUREMENT, not a healthy device.
# hits=0, loose_neighbours=0, boot anchors=0 -> measurement broke (wrong file, closed
# serial port, dead collector). Nothing was observed -> no verdict is meaningful.
# hits=0, loose_neighbours>=1 -> the line exists but is damaged (head
# eaten / bytes spliced in) -> that is a real device-side defect, --exact's purpose.
# hits=0, loose_neighbours=0, anchors>=1 -> the boot DID happen; the marker is
# genuinely absent -> allowed to read as a functional miss.
# Reported as its own exit code (2, the existing "measurement/environment did not happen"
# family) so aggregation by exit code never counts a dead collector as a device failure.
def classify_measurement(n_lines, boot_count, boot_check_on, exact_checked, hits, neighbours):
"""(broken, reason). Only judges when a boot anchor check was actually requested."""
if not boot_check_on:
return False, "anchor check off (max_boot_count=0) -> cannot classify"
if n_lines == 0:
return True, "zero payload lines (collector closed / log truncated / wrong file)"
if boot_count == 0:
return True, ("log readable (%d line(s)) but ZERO boot anchor -> capture did not "
"start at a reset" % n_lines)
if exact_checked and hits == 0 and neighbours == 0:
return False, "anchors present: exact markers genuinely absent (functional miss, not measurement)"
return False, "measurement ok (hits=%d neighbours=%d anchors=%d)" % (hits, neighbours, boot_count)
def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
show_tail=40, out=print, suite_summary="",
selftest_pass=False, selftest_log="",
@@ -519,6 +549,39 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
for ln in lines[-show_tail:]:
out(ln)
# ---- measurement vs device (task-32) ----
n_exact_checked = len(exact or [])
n_exact_hits = sum(1 for e in (res.get("exact_ok") or []) if "hits=0" not in e["detail"])
n_neighbours = 0
for e in (res.get("exact_ok") or []) + (res.get("exact_bad") or []):
m = re.search(r"loose_neighbours=(\d+)", e.get("detail", ""))
if m:
n_neighbours += int(m.group(1))
b_on = max_boot_count > 0
b_cnt = (res.get("boot") or {}).get("count", 0)
broken, m_reason = classify_measurement(len(payloads), b_cnt, b_on,
n_exact_checked > 0, n_exact_hits, n_neighbours)
res["measurement"] = {"broken": broken, "reason": m_reason, "payload_lines": len(payloads),
"boot_anchor_count": b_cnt, "anchor_check_on": b_on,
"exact_checked": n_exact_checked, "exact_hits": n_exact_hits,
"loose_neighbours": n_neighbours}
# A broken capture must never mask real device-side bad news: if the log carries
# Forbidden hits or selftest fault records, that evidence stands and we report FAIL(1)
# with the measurement problem noted on top of it (worst outcome = a real crash quietly
# re-labelled as "no data").
if broken and (hits or selftest_fail):
out("")
out("[WARN] capture also looks incomplete: " + m_reason)
out(" but device-side bad news is present -> verdict stays FAIL, not MEASUREMENT-BROKEN")
broken = False
if broken:
out("")
out("[MEASUREMENT-BROKEN] " + m_reason)
out(" no verdict is meaningful here -- fix the measurement first (open the port,")
out(" assert the collector is alive, capture from a reset). Exit 2, NOT 1:")
out(" a dead collector must not be counted as one more device failure.")
return finish(2)
out("")
if (missing or hits or bad_summary or selftest_fail or boot_issue
or order_issue or timing_issue or exact_bad or pair_bad):
@@ -579,6 +642,19 @@ def selftest(out=print):
chk("blind spot proven: legacy substring PASSES where --exact FAILS",
legacy_present and not exact_ok_here, True)
# measurement-vs-device classification (task-32): the three-counter rule IS this function,
# so it is pinned here -- an all-zero read must not be reportable as a healthy device.
chk("measure: good capture -> not broken", classify_measurement(137, 1, True, True, 1, 0)[0], False)
chk("measure: zero payload lines -> broken", classify_measurement(0, 0, True, True, 0, 0)[0], True)
chk("measure: readable but no boot anchor -> broken",
classify_measurement(1, 0, True, True, 0, 0)[0], True)
chk("measure: anchor check off -> abstain (non-boot logs stay usable)",
classify_measurement(0, 0, False, True, 0, 0)[0], False)
chk("measure: damaged lines are device-side, not 'no data'",
classify_measurement(120, 1, True, True, 0, 3)[0], False)
chk("measure: anchors present + no neighbour = functional miss",
classify_measurement(120, 1, True, True, 0, 0)[0], False)
bad = 0
for name, ok, detail in cases:
out("{} {}".format("[OK] " if ok else "[FAIL]", name))