Files
STM32F4-Base/scripts/check_mermaid.py
T
wandering1 7c505b2457 refactor(adc): 最小二乘拟合移出固件,只留 PC 侧唯一实现
用户裁定(2026-11-13):设备本体只需要校正后的参数,不需要自己拟合
标定曲线,故固件侧最小二乘拟合整体移出,拟合的唯一实现留在 PC 侧
test/host/cal_fit.py。

删除的死代码(生产零调用、上一轮 map 里已被 linker 标 Removing):
- adc_cal.h:adc_cal_fit() 声明、8 个拒绝码、5 个常量
- adc_cal.c:s_div_round() / s_fit_accum() / adc_cal_fit() 共 224 行
- adc_cal_test.c:tc_00b_fit 226 行(184 -> 150 条)

原样保留的设备侧安全闸:
- adc_cal_apply() 折算、adc_cal_set() / adc_cal_set_ratio() 参数合法
  带拦截(防工具写坏参数)、adc_cal_flags() / adc_cal_prot_blocked() /
  adc_cal_rail_ths()。设备不「算」,但必须「用」和「验」。

测试面:
- cal_fit.py 转为唯一实现,新增 --selftest:承接原 C 侧金标向量
  (r_q16=39322000 / b=12)与 4 条拒绝路径,共 11 条断言;已做变异
  测试证明它会红(改坏圆整口径 -> 2 条 FAIL)
- cal_two_point.py 删除,两点工具由多点拟合取代

文档口径统一为「求参在 PC、设备只做折算与合法带校验」,6 篇 docs 同步;
执行卡改名 ADC多点标定执行流程.md(正文同步 5 点最小二乘口径)。

cfg 热重载吞段缺陷的解析器契约与 TC-CONFIG-063 机器钉一并入库。
Program Size 不变(Code=256196):删掉的是从未链接进镜像的死代码。

验证:Keil 0 errors 0 warnings;adc_cal 150/150;selftest 11/11;
ci_local.py RC=0(static 48/48、manual 三根、size-gate、host-unit
4341/4341);doc-links / mermaid / mojibake 全 0。
2026-11-13 08:09:09 +08:00

340 lines
13 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 -*-
"""mermaid 代码块语法预检(提交门禁:mermaid-syntax)
为什么要有(2026-10-30 接线):
docs/子系统设计/屏板链路设计说明.md §1 的 flowchart 里一行带文字点线写成了
`S -.|"..."| T`(合法形态是 `-.->|"..."|` 或 `-. 文字 .->`,`-.|` 不是箭头
token),mermaid 渲染 `Syntax error in text`。该图自 2026-10-29 落库起就是坏的,
doc-links 等文档门禁全绿也拦不住——因为它们查链接与章节,**不查围栏内图表语法**。
本闸只查围栏内的确定性形态,不重造 mermaid 解析器。
查什么(v1 三条,都是「错了必渲染失败」的确定性形态):
R1 图类型头:```mermaid 后首个非空行必须以已知图类型关键字开头
(flowchart/graph/sequenceDiagram/stateDiagram.../classDiagram/erDiagram/
gantt/journey/pie/mindmap/timeline/quadrantChart/gitGraph/C4Context...)。
没有头的块 100% 渲染失败。
R2 `-.` 后直接跟管道(本坑形态):`-.` + 空白 + `|` 不是任何合法箭头;
带文字点线的合法形态只有 `-.->|t|`、`-.-|t|`? 没有、以及 `-. t .->`。
(`-.->` 在 `-.` 后是 `>`,`-. t .->` 在 `-.` 后是空白+文字再 `.->`,
故本判据不误伤这两种——见 --selftest T2。)
R3 容器配对:flowchart/graph 的 `subgraph` 行数 == 独占一行的 `end` 行数;
sequenceDiagram 的行首块起点(alt/opt/loop/par/critical/rect/break)行数
== 行首 `end` 行数(else/and/option 是分支不消费 end)。缺一边必渲染失败。
增量拦截(照 bom-guard / bsp-err / nested-comment 先例,只减不增):
存量钉在 BASELINE(按 文件 + 规则 + 上下文指纹 计数,不按行号);
基线之外的新命中 → 退出码 1。还清某条就删那一行,--prune 打台账。
用法:
python scripts/check_mermaid.py [files...] # 钩子模式(喂暂存的 md)
python scripts/check_mermaid.py --all # 全仓自查(口径 = 钩子实际会喂的文件集)
python scripts/check_mermaid.py --prune # 打基线台账 + 点名已还清可删的行
python scripts/check_mermaid.py --selftest # 自证五条
排除面(与 doc-links 同口径,见 .pre-commit-config.yaml 的 exclude):
芯片手册转录(外部资料,围栏形态不受控)与第三方目录整体不进语料。
"""
import argparse
import pathlib
import re
import sys
REPO = pathlib.Path(__file__).resolve().parent.parent
# 与钩子 exclude 一致:这些前缀下的 md 不进语料
# 另含两类**整份仓库副本**的目录(与 check_doc_links.py 的 SKIP_PARTS 同口径):
# DSH 会话/任务插件把工作树建在仓库内,rglob 会把同一篇文档按别人的任务状态
# 各判一遍——既是假红也是白跑,而它们本来就被 gitignore 挡在钩子语料之外。
EXCLUDE_PREFIXES = (
".opencode/node_modules/",
"Lib/lvgl/",
"MDK-ARM/RTE/",
"docs/芯片手册/",
".agents/",
".dsh-worktrees/",
)
DIAGRAM_HEADS = (
"flowchart", "graph", "sequenceDiagram", "stateDiagram", "stateDiagram-v2",
"classDiagram", "erDiagram", "gantt", "journey", "pie", "mindmap",
"timeline", "quadrantChart", "xychart-beta", "gitGraph", "requirementDiagram",
"C4Context", "C4Container", "C4Component", "packet", "block-beta", "kanban",
"architecture-beta", "radar", "treemap", "sankey-beta", "wardley",
)
RE_FENCE_OPEN = re.compile(r"^\s*```\s*mermaid\s*$")
RE_FENCE_CLOSE = re.compile(r"^\s*```\s*$")
# R2:`-.` 后(允许空白)直接出现管道 = 非法箭头 + 管道文字(本坑形态 `-.|"t"|`)
RE_BAD_DOT_PIPE = re.compile(r"-\.(\s*\|)")
# R3 用:行首关键字
RE_SUBGRAPH = re.compile(r"^\s*subgraph\b")
RE_END_LINE = re.compile(r"^\s*end\b\s*(\[\s*\])?\s*$")
RE_SEQ_BLOCK_START = re.compile(r"^\s*(alt|opt|loop|par|critical|rect|break)\b")
RE_SEQ_END = re.compile(r"^\s*end\b")
RE_COMMENT = re.compile(r"^\s*(%%|rem\b|#)")
def iter_blocks(lines):
"""产出 (fence_line_no_1based, [block lines])。围栏未闭合也报(当空块+坏)。"""
i = 0
n = len(lines)
while i < n:
m = RE_FENCE_OPEN.match(lines[i])
if not m:
i += 1
continue
start = i
j = i + 1
while j < n and not RE_FENCE_CLOSE.match(lines[j]):
j += 1
yield start + 1, lines[start + 1:j], j < n
i = j + 1
def diagram_head(block):
for ln in block:
if not ln.strip() or RE_COMMENT.match(ln):
continue
return ln.strip()
return ""
def hits_for_block(lines, offset):
"""返回 [(abs_line_1based, rule, 上下文指纹)]"""
hits = []
head = diagram_head(lines)
if not head:
hits.append((offset, "R1-no-head", "nohead"))
elif not head.startswith(DIAGRAM_HEADS):
first = re.split(r"[\s\[({]", head, 1)[0]
hits.append((offset, "R1-bad-head", "head:" + first))
is_flow = head.startswith(("flowchart", "graph"))
is_seq = head.startswith("sequenceDiagram")
sg = en = sb = se = 0
for k, raw in enumerate(lines):
ln = raw
if RE_COMMENT.match(ln):
continue
if RE_BAD_DOT_PIPE.search(ln):
ctx = "dotpipe:" + re.sub(r"\s+", " ", ln.strip())[:80]
hits.append((offset + k + 1, "R2-dot-pipe", ctx))
if RE_SUBGRAPH.match(ln):
sg += 1
elif RE_END_LINE.match(ln):
en += 1
if is_seq:
if RE_SEQ_BLOCK_START.match(ln) and not RE_SEQ_END.match(ln):
sb += 1
elif RE_SEQ_END.match(ln):
se += 1
if is_flow:
lo = min(sg, en)
for _ in range(abs(sg - en)):
hits.append((offset, "R3-subgraph-end",
"pair:sg=%d,end=%d" % (sg, en)))
if is_seq:
for _ in range(abs(sb - se)):
hits.append((offset, "R3-seq-end",
"pair:blk=%d,end=%d" % (sb, se)))
return hits
def scan_text(text):
hits = []
lines = text.splitlines()
for start_no, block, closed in iter_blocks(lines):
if not closed:
hits.append((start_no, "R0-unclosed-fence", "unclosed"))
hits.extend(hits_for_block(block, start_no))
return hits
def check_file(path):
"""path: pathlib.Path(相对仓库根)。返回 (blocks数, [(abs_line, rule, ctx)]) 或 None(跳过)。"""
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError) as e:
print(" [skip] %s(%s)" % (path, e))
return None
return count_and_hits(text)
def count_and_hits(text):
hits = []
lines = text.splitlines()
nblk = 0
for start_no, block, closed in iter_blocks(lines):
nblk += 1
if not closed:
hits.append((start_no, "R0-unclosed-fence", "unclosed"))
hits.extend(hits_for_block(block, start_no))
return nblk, hits
# ---------------------------------------------------------------- 基线与报告
# 存量基线(接线当天 2026-10-30 --all 实扫钉入,只减不增;还清删行,--prune 点名):
BASELINE = (
)
def fingerprint(rel, rule, ctx):
# 上下文指纹不逐字节钉死(改图会漂移):R3 的 pair 数字归一化
if rule.startswith("R3"):
return rel + "|" + rule + "|pair"
return rel + "|" + rule + "|" + ctx
def collect_baseline():
d = {}
for rel, rule, ctx in BASELINE:
key = fingerprint(rel, rule, ctx)
d[key] = d.get(key, 0) + 1
return d
def all_md_files():
out = []
for p in REPO.rglob("*.md"):
rel = p.relative_to(REPO).as_posix()
if any(rel.startswith(pre) for pre in EXCLUDE_PREFIXES):
continue
if "/node_modules/" in ("/" + rel) or "/.git/" in ("/" + rel):
continue
out.append(p)
return sorted(out)
def report(files, show_baseline=False, prune=False):
base = collect_baseline()
consumed = dict(base)
new_hits = []
files_with = 0
blocks = 0
for p in files:
rel = p.relative_to(REPO).as_posix()
r = check_file(p)
if r is None or r[0] == 0:
continue
nblk, hits = r
files_with += 1
blocks += nblk
for (ln, rule, ctx) in hits:
key = fingerprint(rel, rule, ctx)
if consumed.get(key, 0) > 0:
consumed[key] -= 1
else:
new_hits.append((rel, ln, rule, ctx))
print("mermaid 预检:%d 个文件共 %d 块围栏,新发 %d 处,基线在册 %d 处"
% (files_with, blocks, len(new_hits), sum(base.values())))
if new_hits:
print("\n--- 新发(不被基线吞)---")
for (rel, ln, rule, ctx) in new_hits:
print(" %s:%d [%s] %s" % (rel, ln, rule, ctx))
print("\n判读:R1=缺图类型头;R2=`-.` 后直接跟 `|`(改 `-.->|` 或 `-. t .->`);"
"R3=subgraph/块起点与独占行 end 不配对;R0=围栏未闭合。")
if prune or (show_baseline and base):
print("\n--- 存量基线台账(只减不增)---")
for (rel, rule, ctx) in BASELINE:
left = consumed.get(fingerprint(rel, rule, ctx), 0)
mark = "已还清,可删" if left == 0 else "仍占 %d" % left
print(" %-70s [%s] %s" % (rel, rule, mark))
if prune:
restorable = [b for b in BASELINE
if consumed.get(fingerprint(*b), 0) == 0]
if restorable:
print("\n[TIGHTEN] 基线 %d 条本轮实测已还清,可下架:%s"
% (len(restorable), restorable))
return 1 if new_hits else 0
# ---------------------------------------------------------------- selftest
def selftest():
ok = True
def scan(text):
return scan_text(text)
# T1 正例:合法 flowchart(含 `-.->|` 与 `-. t .->` 两种合法点线)+ sequence 配对
good = (
"```mermaid\nflowchart TB\n"
' A["甲"] -->|"有字"| B["乙"]\n'
" A -.->|点线有字| B\n"
" A -. 点线文字 .-> B\n"
" subgraph SG[\"组\"]\n B --> C[\"丙\"]\n end\n"
"```\n")
h = [x for x in scan(good) if x[1] != "R0-unclosed-fence" or True]
print(" T1 正例(含三种合法点线 + 配对 subgraph)→ 命中 %d(要求 0)" % len(h))
ok &= (len(h) == 0)
# T2 本坑形态必响,合法形态不误伤
bad = "```mermaid\nflowchart TB\n S -.|\"文字\"| T\n```\n"
h = [x for x in scan(bad) if x[1] == "R2-dot-pipe"]
print(" T2 本坑 `-.|\"..\"|` → R2 命中 %d(要求 1)" % len(h))
ok &= (len(h) == 1)
# T3 subgraph 缺 end 必响
bad3 = "```mermaid\nflowchart LR\n subgraph X[\"x\"]\n A-->B\n```\n"
h = [x for x in scan(bad3) if x[1].startswith("R3-subgraph")]
print(" T3 subgraph 无 end → R3 命中 %d(要求 1)" % len(h))
ok &= (len(h) == 1)
# T4 缺图类型头必响;sequence 配对不误伤
bad1 = "```mermaid\nflowChat TB\n A-->B\n```\n"
h = [x for x in scan(bad1) if x[1] == "R1-bad-head"]
gseq = ("```mermaid\nsequenceDiagram\n participant A\n alt 条件\n A->>A: x\n"
" else 其他\n A->>A: y\n end\n```\n")
h2 = scan(gseq)
print(" T4 假头必响 %d(要求 1)/ sequence alt-else-end 不误伤 %d(要求 0)"
% (len(h), len(h2)))
ok &= (len(h) == 1 and len(h2) == 0)
# T5 基线吞得下 + 只减不增有牙
rel = "fake/file.md"
hit = scan(bad)[0]
key_new = fingerprint(rel, hit[1], hit[2])
b1 = {key_new: 1}
within = 1 if collect_hit_in(b1, rel, hit) else 0
b2 = {}
outside = 0 if collect_hit_in(b2, rel, hit) else 1
print(" T5 基线在册吞 %d(要求 1)/ 基线清空后同命中算新发 %d(要求 1)"
% (within, outside))
ok &= (within == 1 and outside == 1)
print("SELFTEST:", "PASS" if ok else "FAIL")
return 0 if ok else 1
def collect_hit_in(base, rel, hit):
key = fingerprint(rel, hit[1], hit[2])
return base.get(key, 0) > 0
# ---------------------------------------------------------------- main
def main():
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="mermaid 围栏语法预检(只减不增基线)")
ap.add_argument("files", nargs="*", help="md 文件(钩子喂暂存文件)")
ap.add_argument("--all", action="store_true", help="全仓自查(口径=钩子文件集)")
ap.add_argument("--prune", action="store_true", help="打基线台账并点名已还清行")
ap.add_argument("--selftest", action="store_true", help="自证五条")
args = ap.parse_args()
if args.selftest:
sys.exit(selftest())
if args.all:
files = all_md_files()
else:
files = [pathlib.Path(f).resolve() for f in args.files]
files = [p for p in files
if not any(p.relative_to(REPO).as_posix().startswith(pre)
for pre in EXCLUDE_PREFIXES)]
if not files:
print("mermaid 预检:0 个文件(跳过 = 通过)")
sys.exit(0)
sys.exit(report(files, show_baseline=bool(args.all), prune=args.prune))
if __name__ == "__main__":
main()