按用户令提交:把此前未纳管的 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/`)。
This commit is contained in:
1 parent
d26c844f64
commit
e03465c398
46 files changed
+7558
No files matched your search
@@ -0,0 +1,159 @@
|
||||
#!/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())
|
||||
Reference in new issue
Block a user