docs(skills): 明确 @build/@test/@task 为文档别名并新增 precheck/postmerge 命令
AGENTS.md「构建与验证」新增总口径:@build / @test / @task 都是本仓库的 文档别名、不是 shell 命令(2026-10-30 实测:bash 敲 @task / @build 即 command not found,dsh task 被解析成 profile 名报错);`=` 后的命令行 才是可执行形态,DSH 会话显式调用为 /keil-build、/serial-boot-test、 /task-worktree。 - AGENTS.md / docs/README.md / 并行任务工作树方案.md:@task 三处定义位 由「短命令」统一改为「文档别名」,并写入两种实测失败形态 - keil-build / serial-boot-test SKILL.md:定义位点破别名性质;@test 在 skill 内原先没有定义句,补口径 blockquote - task-worktree SKILL.md 与 reference/:whenToUse、标题、症状表、插件桥 去 @task 前缀改指 list / 本 skill;frontmatter 能力清单同步 - task_worktree.py 新增 precheck(树内只读自查:BOM 漂移 / CR / U+FFFD、 docs 逐档门禁、--msgfile 消息 BOM/CRLF/行长/禁词检查,自动补 ci-artifacts/)与 postmerge(主树体积凭证自动诊断:新鲜跳过、红先短链 再只读复检裁定是否升全链;check_doc_links --all;列文件不代提交) - done 快路径打印与门禁拦截提示均指向 postmerge 实测:precheck 对坏消息文件抓到 CRLF、行长 95>80、禁词×3(rc=1); postmerge 端到端「凭证同码新鲜→跳链→doc --all 绿(2967 章节引用 / 214 文档)→列 dirty 文件」rc=0;check_skill_size.py 与 py_compile 通过; frontmatter 改动后 DSH 已用新值真加载过一轮。无 .c/.h 改动,不涉及体积。
This commit is contained in:
@@ -970,6 +970,8 @@ def cmd_done(a) -> int:
|
||||
raise Fail("任务分支相对 {} 没有新提交,无需合并".format(base))
|
||||
if already and ahead == 0:
|
||||
print("[i] {} 已合入 {},跳过合并,直接清场".format(branch, base))
|
||||
print("[i] 该路径在门禁之前就返回——不替你复绿;上一次门禁没跑到的尾巴,"
|
||||
"清场后去主树跑 `postmerge`(或手工 ci_local + check_doc_links --all)")
|
||||
print("=== 清场 ===")
|
||||
teardown_worktree(st, force=a.force, base=base)
|
||||
print("\n完成(分支此前已合入)。")
|
||||
@@ -1020,6 +1022,7 @@ def cmd_done(a) -> int:
|
||||
print("!! 门禁未全绿:工作树与分支**保留**,未清场。")
|
||||
print(" 主干已含合并提交 {}".format(base_after[:12]))
|
||||
print(" 可选:a) 在主工作树修正后追加提交(推荐)")
|
||||
print(" ——体积凭证/台账红就用 `postmerge` 自动诊断回填,别手抄补链")
|
||||
print(" b) 回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12]))
|
||||
return EXIT_FAIL
|
||||
|
||||
@@ -1031,6 +1034,130 @@ def cmd_done(a) -> int:
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
# ------------------------------------------------- 树内预检 / 合并后补绿(机械化)
|
||||
|
||||
|
||||
HYGIENE_BINARY = (".png", ".jpg", ".gif", ".ico", ".ttf", ".bin", ".hex", ".elf",
|
||||
".axf", ".o", ".crf", ".lst", ".zip", ".gz", ".pdf", ".cur")
|
||||
CR_EXEMPT = (".bat", ".cmd") # .gitattributes 强制 CRLF
|
||||
|
||||
|
||||
def _git_bytes(*args, cwd=None):
|
||||
r = subprocess.run(["git"] + [str(x) for x in args], cwd=str(cwd or REPO_ROOT),
|
||||
capture_output=True, stdin=subprocess.DEVNULL)
|
||||
return r.stdout if r.returncode == 0 else None
|
||||
|
||||
|
||||
def _changed_paths(cwd: Path):
|
||||
q = ("-c", "core.quotePath=false") # 中文文件名不许被八进制转义出去
|
||||
tracked = git(*q, "diff", "--name-only", "HEAD", cwd=cwd, check=False).stdout
|
||||
others = git(*q, "ls-files", "--others", "--exclude-standard", cwd=cwd, check=False).stdout
|
||||
return ([p.strip() for p in tracked.splitlines() if p.strip()],
|
||||
[p.strip() for p in others.splitlines() if p.strip()])
|
||||
|
||||
|
||||
def cmd_precheck(a) -> int:
|
||||
"""树内自查(只读):字节体检 + docs 逐档门禁 + 可选提交信息检查。绝不代改。"""
|
||||
cwd = Path.cwd()
|
||||
(cwd / "ci-artifacts").mkdir(exist_ok=True) # 新树没有 ci-artifacts(G 节:ENOENT 坑)
|
||||
tracked, untracked = _changed_paths(cwd)
|
||||
print("[i] {}:改动 {} 个(tracked {} + untracked {})".format(
|
||||
cwd, len(tracked) + len(untracked), len(tracked), len(untracked)))
|
||||
bom3, cr, fffd = bytes.fromhex("efbbbf"), b"\r", bytes.fromhex("efbfbd")
|
||||
problems, docs = 0, []
|
||||
for p in tracked + untracked:
|
||||
f = cwd / p
|
||||
if not f.is_file() or p.lower().endswith(HYGIENE_BINARY):
|
||||
continue
|
||||
b = f.read_bytes()
|
||||
bad = []
|
||||
if cr in b and not p.lower().endswith(CR_EXEMPT):
|
||||
bad.append("CR")
|
||||
n = b.count(fffd)
|
||||
if n:
|
||||
bad.append("U+FFFD x{}".format(n))
|
||||
if p in tracked:
|
||||
hb = _git_bytes("show", "HEAD:" + p, cwd=cwd)
|
||||
if hb is not None and hb.startswith(bom3) and not b.startswith(bom3):
|
||||
bad.append("BOM 漂移(HEAD 有 BOM、工作区没了——bom-guard 会拦)")
|
||||
if p.startswith("docs/") and p.endswith(".md"):
|
||||
docs.append(p)
|
||||
if bad:
|
||||
problems += 1
|
||||
print("!! {}:{}".format(p, " / ".join(bad)))
|
||||
for d in docs:
|
||||
if not run_script("scripts/check_doc_links.py", [d], cwd, "文档门禁 " + d, tail=6):
|
||||
problems += 1
|
||||
if a.msgfile:
|
||||
m = Path(a.msgfile)
|
||||
if not m.is_file():
|
||||
raise EnvError("消息文件不存在:{}".format(m))
|
||||
mb = m.read_bytes()
|
||||
if mb.startswith(bom3):
|
||||
problems += 1
|
||||
print("!! 消息带 BOM(commit -F 要 UTF-8 无 BOM)")
|
||||
if cr in mb:
|
||||
problems += 1
|
||||
print("!! 消息含 CRLF")
|
||||
ml = mb.decode("utf-8", "replace").splitlines()
|
||||
for i, ln in enumerate(ml):
|
||||
lim = 100 if i == 0 else 80
|
||||
if len(ln) > lim:
|
||||
problems += 1
|
||||
print("!! 消息第 {} 行长 {} > {}".format(i + 1, len(ln), lim))
|
||||
break
|
||||
for w in a.ban:
|
||||
c = mb.decode("utf-8", "replace").lower().count(w.lower())
|
||||
print("[i] 禁词 {!r}:{} 次".format(w, c))
|
||||
if c:
|
||||
problems += 1
|
||||
print("完成:问题 {} 个".format(problems))
|
||||
return EXIT_OK if problems == 0 else EXIT_FAIL
|
||||
|
||||
|
||||
def cmd_postmerge(a) -> int:
|
||||
"""主树合并后补绿:体积凭证自动诊断(短链/全链)+ 文档全量。列文件,不提交。
|
||||
|
||||
裁定外包给现有门禁:凭证红 → 先短链(--update 重写)→ 用只读的
|
||||
check_mem_numbers 复检裁定数字是否真变——红了才升级全链(--write 族)。
|
||||
"""
|
||||
main = main_worktree()
|
||||
if Path.cwd().resolve() != main.resolve():
|
||||
raise EnvError("postmerge 必须在主工作树跑:{}".format(main))
|
||||
if not list((main / "MDK-ARM").glob("**/*.map")):
|
||||
raise EnvError("主树没有 .map——先 @build 0/0 再回来(C.5 的前提)")
|
||||
gate = "scripts/check_size_gate.py"
|
||||
if not a.force and run_script(gate, ["--local"], main, "体积凭证检查(同码)", tail=8):
|
||||
print("[ok] 凭证同码新鲜,无需补链")
|
||||
else:
|
||||
run_script("scripts/ram_baseline.py", ["--update"], main, "短链:重写体积凭证", tail=6)
|
||||
mem_ok = (a.skip_mem or run_script("scripts/check_mem_numbers.py", [], main,
|
||||
"受管区一致性复检(只读)", tail=8))
|
||||
if not mem_ok:
|
||||
print("[i] 快照与 .map 真漂了 → 升级全链(数字变了才动受管区)")
|
||||
run_script("scripts/check_mem_numbers.py", ["--write"], main, "同步内存快照")
|
||||
run_script("scripts/ram_baseline.py", ["--write"], main, "同步 RAM 预算台账")
|
||||
if a.coverage:
|
||||
run_script("scripts/check_test_coverage.py", ["--update-baseline"], main,
|
||||
"覆盖率基线刷新", tail=6)
|
||||
if not run_script(gate, ["--local"], main, "凭证复检(期望绿)", tail=8):
|
||||
print("!! 凭证复检仍红——贴输出查 check_size_gate 原因,别硬提交")
|
||||
return EXIT_FAIL
|
||||
ok = True
|
||||
if not a.skip_docs:
|
||||
ok = run_script("scripts/check_doc_links.py", ["--all"], main, "文档全库检查", tail=8)
|
||||
dirty = git("-c", "core.quotePath=false", "diff", "--name-only", "HEAD",
|
||||
cwd=main, check=False).stdout.splitlines()
|
||||
dirty = [x.strip() for x in dirty if x.strip()]
|
||||
if dirty:
|
||||
print()
|
||||
print("[i] 回填产物(提交属 Ask first,本命令不代提交):")
|
||||
for p in dirty:
|
||||
print(" - " + p)
|
||||
print(" 建议:git add <上面列的文件> && git commit -F <正文文件>")
|
||||
return EXIT_OK if ok else EXIT_FAIL
|
||||
|
||||
|
||||
def cmd_abort(a) -> int:
|
||||
st = load_state(a.task_id)
|
||||
wt = Path(st["path"])
|
||||
@@ -1116,6 +1243,18 @@ def build_parser():
|
||||
p.add_argument("task_id")
|
||||
p.add_argument("--force", action="store_true")
|
||||
p.set_defaults(func=cmd_abort)
|
||||
|
||||
p = sub.add_parser("precheck", help="树内自查(只读):字节体检 + docs 逐档门禁 + 可选消息检查")
|
||||
p.add_argument("--msgfile", default=None, help="附带检查提交信息文件(BOM/CRLF/行长/禁词)")
|
||||
p.add_argument("--ban", action="append", default=[], help="禁词,可重复")
|
||||
p.set_defaults(func=cmd_precheck)
|
||||
|
||||
p = sub.add_parser("postmerge", help="主树合并后补绿:体积凭证自动诊断(短/全链)+ 文档全量;不代提交")
|
||||
p.add_argument("--force", action="store_true", help="凭证新鲜也强制走一遍 --update")
|
||||
p.add_argument("--coverage", action="store_true", help="附带覆盖率基线刷新")
|
||||
p.add_argument("--skip-docs", action="store_true", help="跳过 check_doc_links --all")
|
||||
p.add_argument("--skip-mem", action="store_true", help="只挪凭证,跳过受管区复检/全链")
|
||||
p.set_defaults(func=cmd_postmerge)
|
||||
return ap
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user