Files
workbuddy_skills/dsh-knowledge/references/dsh-knowledge-upkeep/split_skill.py
T
admin e03465c398 按用户令提交:把此前未纳管的 9 个技能目录一并入库
用户令逐字:「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/`)。
2026-10-08 22:29:08 +08:00

160 lines
7.4 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 -*-
"""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())