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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user