用户令逐字:「E:/ProgramData/.workbuddy/skills 提交仓库是指的这里」—— 即本目录就是仓库(2026-10-07 已在本目录建仓,见当日日志 §22),本轮把余下未纳管的 9 个技能一并提交。 本次入库(9 个技能,46 个文件): 1、`AI HOT` 2、`draw-ui` 3、`dsh-diagnose` 4、`dsh-knowledge` 5、`dsh-local-env` 6、`dsh-opensource-release` 7、`dsh-workflow` 8、`oil-motion` 9、`skills-security-check` 提交前核对: · **凭据类扫描**(`*.env` / `*token*` / `*.key` / `*secret*` / `*.pem`)⇒ **零命中** ✓; · 体积合计约 20 MB(`draw-ui` 12M + `oil-motion` 6.5M 是大头,形态为配图/素材 —— 仓库 `.gitignore` 里明写「`assets/*.png` 是内容不是产物」⇒ 属刻意入库); · 运行产物仍按既定规则排除(`__pycache__` / `logs/` / `tmp/` / `.venv/` / `*.egg-info` / `uv.lock` / `.workbuddy/`)。
160 lines
7.4 KiB
Python
160 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""split_skill.py — 技能重组:把千行级技能的 SKILL.md 拆成「主干 + references/ 详情档」。
|
||
|
||
用法:
|
||
<python> split_skill.py --spec <spec.json> [--dry-run]
|
||
|
||
设计(照 `dsh-knowledge-upkeep` §10):
|
||
· **纯机械搬运** —— 只按行号区间搬移,逐行未改;不重写、不润色、不删字。
|
||
· **可重复跑(幂等)** —— 永远以 `<backup_dir>/<skill>-SKILL.md` 为拆前基准;
|
||
首次运行自动生成该备份。⚠️ 绝不以「活文件」为基准(否则第二次跑会二次切割)。
|
||
· **四项自校验**:① 行数守恒 ② 逐行包含 0 丢失 ③ 节可寻址 ④ frontmatter 唯一。
|
||
|
||
spec.json:
|
||
{
|
||
"skills_dir": "E:/ProgramData/.workbuddy/skills",
|
||
"backup_dir": "…/tmp/_keep-YYYYMMDD/split-bak",
|
||
"skill": "dsh-change-workflow",
|
||
"main_desc": "§1 … · §2 …", // 写进详情档头的「主文件」说明
|
||
"moves": [
|
||
{"ranges": [[14,92]], "ref": "00-平台速查.md",
|
||
"title": "平台速查(硬编码事实,勿猜)",
|
||
"covers": ["平台速查(硬编码事实,勿猜)"], // 用于主干索引表的「覆盖的原章节」列
|
||
"pointer": "> 📂 **…已下沉** → `references/00-平台速查.md`"}
|
||
]
|
||
}
|
||
ranges = 1-based 闭区间,可多段,**不得重叠**;pointer 支持 \n 多行。
|
||
"""
|
||
import argparse
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
from collections import Counter
|
||
|
||
HDR = ("# {title}\n\n"
|
||
"> **归属**:技能 `{skill}` 的详情档(**按需读**,不是每次都要读)。\n"
|
||
"> **本档覆盖**:{covers}(原行 {rng})。\n"
|
||
"> **主文件 / 判据与流程主干** = `../SKILL.md`({main})。\n"
|
||
"> **来源**:技能重组把 `../SKILL.md` 的 {rng} 段**逐行原样**下沉到本文件,未改一字。\n"
|
||
"> **跨档引用**:正文里的「§N / 见 §8 坑 N / 见下表」等编号,用 `../SKILL.md` 末节「详情索引」的原章节列定位。\n\n---\n")
|
||
|
||
md5 = lambda b: hashlib.md5(b).hexdigest()
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--spec", required=True)
|
||
ap.add_argument("--dry-run", action="store_true")
|
||
a = ap.parse_args()
|
||
|
||
cfg = json.load(open(a.spec, encoding="utf-8"))
|
||
skill = cfg["skill"]
|
||
d = os.path.join(cfg["skills_dir"], skill)
|
||
src = os.path.join(d, "SKILL.md")
|
||
os.makedirs(cfg["backup_dir"], exist_ok=True)
|
||
bak = os.path.join(cfg["backup_dir"], "%s-SKILL.md" % skill)
|
||
|
||
if not os.path.exists(bak): # 首次:以活文件为基准建备份
|
||
open(bak, "wb").write(open(src, "rb").read())
|
||
orig_bytes = open(bak, "rb").read() # 之后永远以备份为基准(幂等)
|
||
orig = orig_bytes.decode("utf-8").split("\n")
|
||
if orig and orig[-1] == "":
|
||
orig = orig[:-1]
|
||
n = len(orig)
|
||
|
||
# ── 区间校验
|
||
moved, starts = {}, {}
|
||
for mv in cfg["moves"]:
|
||
for rng in mv["ranges"]:
|
||
lo, hi = rng
|
||
assert 1 <= lo <= hi <= n, "区间越界 %s(原文件 %d 行)" % (rng, n)
|
||
for i in range(lo, hi + 1):
|
||
assert i not in moved, "区间重叠于第 %d 行" % i
|
||
moved[i] = mv["ref"]
|
||
starts[min(r[0] for r in mv["ranges"])] = mv
|
||
|
||
# ── 生成主干
|
||
out, ptrs = [], []
|
||
for i in range(1, n + 1):
|
||
if i in starts:
|
||
out.append(starts[i]["pointer"])
|
||
out.append("")
|
||
ptrs.append(starts[i]["pointer"])
|
||
if i not in moved:
|
||
out.append(orig[i - 1])
|
||
main_len = len(out)
|
||
|
||
# ── 生成详情档
|
||
refs = {}
|
||
for mv in cfg["moves"]:
|
||
refs.setdefault(mv["ref"], []).append(mv)
|
||
index, ref_body = [], {}
|
||
for refname, mvs in sorted(refs.items()):
|
||
rngs = sorted(r for mv in mvs for r in mv["ranges"])
|
||
body = [l for (lo, hi) in rngs for l in orig[lo - 1:hi]]
|
||
rng_txt = " + ".join("L%d–L%d" % (lo, hi) for (lo, hi) in rngs)
|
||
covers = " · ".join(dict.fromkeys(c for mv in mvs for c in mv.get("covers", [])))
|
||
head = HDR.format(title=mvs[0]["title"], skill=skill, covers=covers,
|
||
rng=rng_txt, main=cfg.get("main_desc", ""))
|
||
ref_body[refname] = body
|
||
index.append((refname, covers, len(body), rng_txt))
|
||
if not a.dry_run:
|
||
rd = os.path.join(d, "references")
|
||
os.makedirs(rd, exist_ok=True)
|
||
with open(os.path.join(rd, refname), "w", encoding="utf-8", newline="\n") as f:
|
||
f.write(head + "\n".join(body) + "\n")
|
||
|
||
# ── 主干末节:详情索引(= 跨档引用的定位表)
|
||
out += ["", "## 详情索引(references/)", "",
|
||
"> 本技能 = **主干(本文件)+ 详情档**。主干只留判据 / 流程主干 / 命令骨架;长表、案例、实测记录、历史细节在下面各档。",
|
||
">", "> **跨档引用怎么查**:正文里的「§N」「见 §8 坑 N」「见下表」等编号,按本表「覆盖的原章节」列定位。",
|
||
"", "| 详情档 | 覆盖的原章节 | 原行段 | 行数 |", "|---|---|---|---|"]
|
||
for refname, covers, ln, rng_txt in index:
|
||
out.append("| `references/%s` | %s | %s | %d |" % (refname, covers.replace("|", "/"), rng_txt, ln))
|
||
out.append("")
|
||
idx_block = len(out) - main_len
|
||
text = "\n".join(out).rstrip("\n") + "\n"
|
||
new_lines = text.split("\n")[:-1]
|
||
|
||
# ── 四项自校验
|
||
core = main_len - 2 * len(cfg["moves"])
|
||
det = sum(len(v) for v in ref_body.values())
|
||
A = Counter(orig)
|
||
B = Counter([l for l in new_lines if l not in ptrs]
|
||
+ [l for rn in ref_body for l in ref_body[rn]])
|
||
lost = [k for k in A if A[k] > B[k]]
|
||
heads = [re.sub(r"^#+\s*", "", l).strip() for l in orig if re.match(r"^#{1,3} ", l)]
|
||
blob = text + "".join("\n".join(v) for v in ref_body.values())
|
||
miss = [h for h in heads if h and h not in blob]
|
||
fm = [l for l in new_lines[:14] if l.startswith("version:")]
|
||
|
||
checks = [
|
||
("① 行数守恒", core + det == n, "主干内容 %d + 详情 %d = %d(原 %d)" % (core, det, core + det, n)),
|
||
("② 逐行包含", not lost, "丢失 %d 行;新增(指针 %d + 索引 %d)" % (len(lost), 2 * len(cfg["moves"]), idx_block)),
|
||
("③ 节可寻址", not miss, "标题 %d 个,未命中 %d%s" % (len(heads), len(miss), (" " + repr(miss[:3])) if miss else "")),
|
||
("④ frontmatter", len(fm) == 1, "version 行 = %s" % (fm or "缺失/重复")),
|
||
]
|
||
print("=" * 68)
|
||
print("技能 %s:原 %d 行 md5=%s" % (skill, n, md5(orig_bytes)))
|
||
for name, ok, info in checks:
|
||
print(" %s %s ── %s" % ("OK " if ok else "FAIL", name, info))
|
||
print(" 主干 SKILL.md = %d 行 %s" % (len(new_lines), "(dry-run,未落盘)" if a.dry_run else "md5=%s" % md5(text.encode("utf-8"))))
|
||
for refname, covers, ln, rng_txt in index:
|
||
print(" - %-40s %4d 行 %s" % (refname, ln, rng_txt))
|
||
if not a.dry_run:
|
||
with open(src, "w", encoding="utf-8", newline="\n") as f:
|
||
f.write(text)
|
||
bad = [c[0] for c in checks if not c[1]]
|
||
if bad:
|
||
print("⛔ 未通过:%s ⇒ 停下来看,别硬推" % "、".join(bad))
|
||
return 1
|
||
print("✅ 四项校验全绿" + ("(dry-run)" if a.dry_run else ";记得三处同步 + 三方 md5 对账"))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|