chore(全仓): @build/@test 旧文档别名清退为技能调用名

现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
This commit is contained in:
2026-10-30 13:03:12 +08:00
parent dc964090ea
commit 4a4ad6bc5f
123 changed files with 455 additions and 455 deletions
+3 -3
View File
@@ -35,7 +35,7 @@ CubeMX 重生成存活审计(check_cubemx_survival.py)
注意
----
`MDK-ARM/*.uvprojx` **不参与漂移检测**(`no_baseline=True`):该文件每次
`@build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到本机 DFP
`/keil-build` 后被 Keil UV4 自动改写(XML 格式归一化 + `PackID` 降级到本机 DFP
版本),内容每次构建都不同。若参与检测,则**每次构建后提交都会被拦**。
本项只保留 `must_contain` 断言("源文件仍挂载")。
"""
@@ -242,7 +242,7 @@ CHECKS = [
kind="generated",
path="MDK-ARM/STM32F407-Demo.uvprojx",
must_contain=["task_watchdog.c"],
no_baseline=True, # 见下方说明:本文件每次 @build 后被 Keil 改写
no_baseline=True, # 见下方说明:本文件每次 /keil-build 后被 Keil 改写
hint="task_watchdog.c 须挂在 MDK 工程 App 组;CubeMX 重生成可能重建 .uvprojx。",
),
dict(
@@ -490,7 +490,7 @@ def evaluate():
# ---- 基线快照 ----
# `no_baseline=True` 的项不参与漂移检测。理由:`MDK-ARM/*.uvprojx`
# 每次 `@build` 后被 Keil UV4 自动改写(XML 格式归一化 + PackID 降级到
# 每次 `/keil-build` 后被 Keil UV4 自动改写(XML 格式归一化 + PackID 降级到
# 本机 DFP 版本),内容每次构建都不同 → 若参与漂移检测,则**每次构建后
# 提交都会被拦**。本项只保留 must_contain 断言("源文件仍挂载")。
if not c.get("no_baseline"):
+2 -2
View File
@@ -336,10 +336,10 @@ def main(argv):
map_path = pathlib.Path(args.map)
if not map_path.exists():
print(f"[FAIL] 找不到 .map 事实源:{map_path}", file=sys.stderr)
print(" 先跑一次 @build(keil-build skill)生成 .map,再跑本门禁。", file=sys.stderr)
print(" 先跑一次 /keil-build(keil-build skill)生成 .map,再跑本门禁。", file=sys.stderr)
print(" 这不是代码缺陷:.map 是**每棵工作树各自的构建产物**(不入版本库),",
file=sys.stderr)
print(" 新开的工作树(task-worktree new)从未构建过时必然走到这里——先 @build",
print(" 新开的工作树(task-worktree new)从未构建过时必然走到这里——先 /keil-build",
file=sys.stderr)
print(" 一次即可;随后按需要跑 scripts/ram_baseline.py --update 回填台账。",
file=sys.stderr)
+9 -9
View File
@@ -5,7 +5,7 @@
要消除的盲区
------------
`mem-numbers` / `mem-budget` 两根闸的**事实源都是 Keil 链接产物 `.map`**
(gitignore,只有真做过 `@build` 的机器上才有)。于是两处都是"没跑 = 通过":
(gitignore,只有真做过 `/keil-build` 的机器上才有)。于是两处都是"没跑 = 通过":
· CI 侧:runner 没有 Keil,workflow 只能把这根钩子**跳过**,历史上只打一条
`::warning::`(上一轮的处置)——"CI 全绿"仍不等于"体积守住了";
@@ -34,7 +34,7 @@
② 那次测量的 RAM 受管区是否 <=92%(用已提交的测量数字守红线)。
2. 本门禁**不能**判:
· CI 不重算绝对体积——runner 无 Keil/无 `.map`,绝对数字的可信度来自本地那次
`@build`;本脚本把盖戳卡在"新鲜 .map + 未超线"之后,凭证不是随手可写的。
`/keil-build`;本脚本把盖戳卡在"新鲜 .map + 未超线"之后,凭证不是随手可写的。
· **编译器选项不在指纹范围内**:`MDK-ARM/STM32F407-Demo.uvprojx` 每次构建都被 UV4
改写且按约定不入库(keil-build skill),把它放进指纹会让门禁在"刚刚构建完"
的那一刻自己变红;代价是改优化等级/预处理宏不触发本门禁,这类改动必须人工
@@ -257,7 +257,7 @@ def cmd_stamp(args):
"""用新鲜 .map 盖同码戳;三道前置任一不过即拒绝(见模块 docstring)。"""
map_path = pathlib.Path(args.map)
if not map_path.exists():
print("[FAIL] 没有 .map(%s)——先 @build 再盖戳" % map_path, file=sys.stderr)
print("[FAIL] 没有 .map(%s)——先 /keil-build 再盖戳" % map_path, file=sys.stderr)
return 1
m = measure(map_path)
if m is None:
@@ -266,7 +266,7 @@ def cmd_stamp(args):
map_mt = map_path.stat().st_mtime
src_mt = newest_source_mtime()
if src_mt > map_mt:
print("[FAIL] .map 早于最新受管源码——这份构建不含最新改动,重新 @build",
print("[FAIL] .map 早于最新受管源码——这份构建不含最新改动,重新 /keil-build",
file=sys.stderr)
print(" .map mtime=%d 最新源码 mtime=%d" % (int(map_mt), int(src_mt)),
file=sys.stderr)
@@ -290,7 +290,7 @@ def cmd_stamp(args):
file=sys.stderr)
print(" 修复:先 git add <这些文件>(凭证描述的就是你正要提交的那份),再盖戳;",
file=sys.stderr)
print(" 或把它们还原。@build 与盖戳都应在同一份内容上做。")
print(" 或把它们还原。/keil-build 与盖戳都应在同一份内容上做。")
return 1
fp, n = tree_fingerprint()
write_attestation({
@@ -339,7 +339,7 @@ def _cmd_parity(args, ci_mode):
if doc is None:
return _report(args, ci_mode, [
"体积同码凭证不可用:" + err,
" 修复:本地 @build 之后跑 python scripts/ram_baseline.py --update",
" 修复:本地 /keil-build 之后跑 python scripts/ram_baseline.py --update",
" (或单独跑 python scripts/check_size_gate.py --stamp)",
])
fp, n = tree_fingerprint()
@@ -350,14 +350,14 @@ def _cmd_parity(args, ci_mode):
"指纹范围内有 %d 处**未 staged** 的改动/未跟踪文件,凭证覆盖不到它们" % dirt,
" " + "、".join(samples) + ("…" if dirt > len(samples) else ""),
" 含义:这些内容既没进索引(凭证按索引生成),也就没被那次测量算进去",
" 修复:git add 之后 @build,再跑 python scripts/ram_baseline.py --update",
" 修复:git add 之后 /keil-build,再跑 python scripts/ram_baseline.py --update",
]
if fp != doc["fingerprint"]:
problems += [
"本次代码(指纹 %s…,%d 文件)与体积测量(指纹 %s…,%s)不同码" % (
fp[:12], n, str(doc["fingerprint"])[:12], doc.get("built_at", "?")),
" 含义:这份源码没有被真实链接测量过(或测量完又改了代码/合入了别的改动)",
" 修复:@build 0/0 之后跑 python scripts/ram_baseline.py --update 再提交",
" 修复:/keil-build 0/0 之后跑 python scripts/ram_baseline.py --update 再提交",
" (指纹取 git 索引 blob:同一份内容在任何工作树/runner 上算法一致,"
"不受 CRLF/LF 检出差异影响)",
]
@@ -441,7 +441,7 @@ def main(argv=None):
ap = argparse.ArgumentParser(
description="体积门禁强制路径(同码凭证 + CI 红线复核)")
g = ap.add_mutually_exclusive_group()
g.add_argument("--stamp", action="store_true", help="用新鲜 .map 盖同码戳(@build 后)")
g.add_argument("--stamp", action="store_true", help="用新鲜 .map 盖同码戳(/keil-build 后)")
g.add_argument("--local", action="store_true", help="本地/ci_local 自查:工作树须与测量同码")
# ⚠ help 字符串里 % 必须写成 %%(argparse 会对 help 做 %-插值;裸 % + 中文
# 如"92% 红"在**无参/非法参的 print_help 路径**上才炸:ValueError
+3 -3
View File
@@ -31,7 +31,7 @@
message 喂给钩子。
刻意**不含**(与 CI 同理由,见 workflow 文件头):
- `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
- `/keil-build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
- `mem-numbers` / `mem-budget` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map
就照常跑**,与 CI 的自适应逻辑一致)。⚠ 这两根闸在"无 .map"时是 **SKIP 而不是
PASS**(等于这一趟什么都没查);那个缺口由上面的体积门禁(size)补住:同码凭证
@@ -508,7 +508,7 @@ def run_static_gates():
else:
print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers / "
"mem-budget(与 CI 一致)")
print("[INFO] :warning: 体积门禁未执行——改代码后请本地 @build + "
print("[INFO] :warning: 体积门禁未执行——改代码后请本地 /keil-build + "
"check_mem_numbers.py --scan + ram_baseline.py --gate 兜底")
skip = env.get("SKIP", "")
add = "mem-numbers,mem-budget"
@@ -655,7 +655,7 @@ def run_size_gate():
return 2
rc = _run([PY, str(script), "--local"])
if rc == 1:
print(" (1 = 没有凭证,或代码与测量不同码:@build 0/0 之后跑 "
print(" (1 = 没有凭证,或代码与测量不同码:/keil-build 0/0 之后跑 "
"python scripts/ram_baseline.py --update)")
print("[SUMMARY] size-gate: rc={}".format(rc))
return rc
+9 -9
View File
@@ -10,7 +10,7 @@ RAM 预算门禁 + 趋势台账(登记簿条目「缩堆迁 CCM 后半(暂
无 `.map`(未做 Keil 构建)→ 自适应跳过(与 `mem-numbers` 同口径,
CI 盲区口径见 `scripts/check_size_gate.py` 的 docstring)。
`--gate` 同时校验台账受管区与 `.map` 一致(防漂移,同 `mem-numbers` 思路)。
2. **趋势基线** `scripts/ram_baseline.tsv`(`--update` 每次 @build 后追加一行;
2. **趋势基线** `scripts/ram_baseline.tsv`(`--update` 每次 /keil-build 后追加一行;
`--trend` 看趋势并高亮 RW+ZI 较上一行增长 >1 KB 的对象——方案 §4.0.3
"ZI 增量 >1KB 必须解释"的机械化)。
3. **台账受管区**(`--write` 同步 `docs/内存管理/RAM预算台账.md` 的
@@ -136,7 +136,7 @@ def warn_unattributed(info):
"「应用」(台账 §2:应用 = App/ 其余),合计 %d B:" % (len(missing), total))
for obj, nbytes in missing:
print(" %-28s %6d B" % (_obj_base(obj), nbytes))
print(" 修法:补 subsystem_of() 前缀 -> 重跑 @build -> --update / --write 回填台账")
print(" 修法:补 subsystem_of() 前缀 -> 重跑 /keil-build -> --update / --write 回填台账")
return len(missing)
@@ -232,7 +232,7 @@ def cmd_gate(args):
print(" 强制兜底(无需 Keil,改代码后必然拦红):")
print(" 本地 scripts/ci_local.py 的体积门禁步 / CI 的「体积门禁」步,")
print(" 都跑 scripts/check_size_gate.py(同码凭证 + 92% 红线复核)。")
print(" 改过代码请先 @build,再 python scripts/ram_baseline.py --update。")
print(" 改过代码请先 /keil-build,再 python scripts/ram_baseline.py --update。")
return 0
info = collect(map_path)
if not info["regions"]:
@@ -284,13 +284,13 @@ def cmd_gate(args):
ir = info["regions"].get("RW_IRAM1")
if rows and ir and int(rows[-1]["iram1"]) != ir["size"]:
print("[WARN] ram_baseline.tsv 末行落后于当前 .map —— "
"@build 后请跑 `python scripts/ram_baseline.py --update`")
"/keil-build 后请跑 `python scripts/ram_baseline.py --update`")
else:
print("[WARN] ram_baseline.tsv 不存在 —— 跑 "
"`python scripts/ram_baseline.py --update` 建档")
newest = _newest_source_mtime()
if newest and newest > map_path.stat().st_mtime:
print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 @build 再提交")
print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 /keil-build 再提交")
warn_unattributed(info)
@@ -303,7 +303,7 @@ def cmd_gate(args):
def cmd_write(args):
map_path = _map_or_skip(args)
if map_path is None:
print("[FAIL] 找不到 .map 事实源,先 @build(.map 是每棵工作树各自的构建产物,不入版本库;",
print("[FAIL] 找不到 .map 事实源,先 /keil-build(.map 是每棵工作树各自的构建产物,不入版本库;",
file=sys.stderr)
print(" 新开的 task-worktree 工作树从未构建过时必然走到这里,不是代码缺陷)",
file=sys.stderr)
@@ -352,7 +352,7 @@ def _top_field(top):
def cmd_update(args):
map_path = _map_or_skip(args)
if map_path is None:
print("[FAIL] 找不到 .map 事实源,先 @build(.map 是每棵工作树各自的构建产物,不入版本库;",
print("[FAIL] 找不到 .map 事实源,先 /keil-build(.map 是每棵工作树各自的构建产物,不入版本库;",
file=sys.stderr)
print(" 新开的 task-worktree 工作树从未构建过时必然走到这里,不是代码缺陷)",
file=sys.stderr)
@@ -388,7 +388,7 @@ def cmd_update(args):
f"({region_pct(reg['RW_CCM']):.1f}%) → {TSV_PATH.relative_to(REPO_ROOT)}")
# 强制路径(2026-10-25):同一次「真实测量」顺带刷新**体积同码凭证**
# scripts/size_attestation.json——这样「每次 @build 后跑 --update」这条**既有
# scripts/size_attestation.json——这样「每次 /keil-build 后跑 --update」这条**既有
# 纪律**同时喂饱那条无需 Keil 也能判的强制门禁(check_size_gate --ci /
# --local,即 ci_local 的体积门禁步与 CI 的体积门禁步),不新增人工步骤。
# 盖戳失败只告警、不改本命令返回值:凭证新鲜度由门禁自己 fail-closed 兜,
@@ -471,7 +471,7 @@ def main(argv=None):
g = ap.add_mutually_exclusive_group()
g.add_argument("--gate", action="store_true", help="92% 阈值门禁(pre-commit 入口)")
g.add_argument("--write", action="store_true", help="同步台账受管区")
g.add_argument("--update", action="store_true", help="追加 tsv 趋势行(@build 后跑)")
g.add_argument("--update", action="store_true", help="追加 tsv 趋势行(/keil-build 后跑)")
g.add_argument("--trend", action="store_true", help="打印趋势 + 高亮 >1 KB 增长")
ap.add_argument("-n", type=int, default=10, help="--trend 显示行数")
ap.add_argument("--map", default=str(MAP_DEFAULT), help=".map 路径")
+1 -1
View File
@@ -240,7 +240,7 @@ def main():
"已落盘,请接着登记注册表族并重跑 scripts/check_ids.py"
if args.apply else "干跑结束,确认后加 --apply"))
print("提醒:改名后必跑 check_ids.py + check_doc_links.py --all + ci_local.py;"
"改到 test/board 或 Drivers/ 还要 @build。")
"改到 test/board 或 Drivers/ 还要 /keil-build。")
return 0
+1 -1
View File
@@ -468,7 +468,7 @@ def main(argv=None):
print("ℹ 引用关系图是生成物,未改写 → 重跑 & 'tools\\refgraph\\gen_refgraph.bat'")
if old_abs.suffix.lower() in (".c", ".h") or new_abs.suffix.lower() in (".c", ".h"):
print("ℹ 改了 .c/.h:已同步 uvprojx 与 CMakeLists;若**新增目录**还需把目录加进 "
"uvprojx 的 <IncludePath>(见 new-source-file skill),随后跑 @build 验证")
"uvprojx 的 <IncludePath>(见 new-source-file skill),随后跑 /keil-build 验证")
print("ℹ 正文里的**裸文件名/昵称引用**(如「内存方案 §2.1」「app_main.c/h」)无法机械判定,"
"已尽量改写,仍建议 grep 一遍旧名:git grep -n '<旧文件名>'")
+1 -1
View File
@@ -14,7 +14,7 @@
真停滞嫌疑段,见下。
输入契约(serial-assistant 落盘日志 = 纯文本完整行;两种时钟源自动择一):
- 首选采集相对钟:行首 [+NNNms](@test/采集管线相对时戳;相对钟回退 >5s、或出现
- 首选采集相对钟:行首 [+NNNms](串口回归/采集管线相对时戳;相对钟回退 >5s、或出现
reset cause 锚点行 = 新会话边界——烧录/复位/clear 都会造成不连续);
- 无相对钟时退化为设备绝对钟:行内 [YYYY-MM-DD HH:MM:SS](墙钟未就绪的
[----/--/-- --:--:--] 不算时戳;RTC 同步类向前跳变按会话边界处理、不计静默;