1634088bd7
屏板链路设计说明.md -> 屏板链路设计.md(去档名「说明」,与 ADC 节拍 / 运行与自检记录两篇口径一致);控制台单写者日志设计说明.md 同改。 两档并存期间旧名引用 0 残留。 - 屏板链路 §3.2 台架日志口(RS232#1 / CN1 端子 / COM3)移入控制台档 §3.4:日志口的接线属 UART5 控制台通道,不属屏板链路;屏板档只留 「别去 CN1 找 USART1 端子」这句接线防错提示。 - 屏板链路 §7.1 由「根因账本 + 修复前后对比」改写为现状描述:延迟 应答机制表 + 耗时分解表 + 两个计数口径 + 已知边界,40 行压到 24 行。 顺带修正 §8 的错引:原指 `网络自愈架构设计.md` §8(实为「延后项」, 不含 uiTask 内容)改为 §4.1 `net_reconcile_tick`。 - §9.1 增补长稳行(串口日志 2026-11-13 18:31 -> 2026-11-15 21:43, 单次上电 51.2 h:rx=658262 ok=658262、crc/len/fmt/err 全 0、 drop=7 仅启动期、kerr/abor/bad=0、全程 off=0);§9.2 销项三条 (仪器项、2 h 长稳、len=7 观察项),新增「按键通路」一条——51.2 h 日志 keys=0 kq=0 dfok=0 dfold=0,延迟应答在真机上从未执行过。 - 删除 sb_frame_b_build_logo() 及 SB_LOGO_MAGIC_* / SB_SCREEN_W/H 与对应单测(TC-SBPROTO-022/023 全组、024c/024d),组帧入口只剩 sb_frame_b_build_image()。sb_proto 单测 59 -> 45 条,45/45 通过。 - cfg_file.c 出厂模板三条中文字面量改 \uXXXX 转义:消除全量重编时的 4 条 #870-D invalid multibyte character sequence,运行期字节不变, 源码回到纯 ASCII(符合「字符串字面量必须纯 ASCII」)。 验证:keil-build 0 Error 0 Warning;sb_proto 45/45、sb_link 164/164; pre-commit 48 钩子全 Passed;check_doc_links --sections-strict 通过。
340 lines
13 KiB
Python
340 lines
13 KiB
Python
#!/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()
|