按用户令提交:把此前未纳管的 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,109 @@
|
||||
# 技能重组 · 千行技能拆分(SKILL.md 主干 + references/ 详情档)
|
||||
|
||||
> **归属**:技能 `dsh-knowledge-upkeep` 的详情档(**按需读**,不是每次都要读)。
|
||||
> **本档覆盖**:§10 的执行清单 · 通用拆分器用法 · 2026-09-22 首次实战的实测数据与决策记录 · 坑。
|
||||
> **主文件 / 判据** = `../SKILL.md`(§2 分层判据 · §8.6 注入预算 · §9 技能集体检 · **§10 技能重组**)。
|
||||
> **来源**:2026-09-22「技能重组线」首跑后沉淀(首次对象 = `dsh-change-workflow` / `dsh-opensource-release`)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要有它(一句话)
|
||||
|
||||
技能长到千行级 ⇒ **每次加载都全量注入**,而里面大半是长表 / 实测记录 / 历史细节。拆成「主干 + 详情档」不是审美,是**把注入预算还给判据**(同 §8.6:长文件后半段等于不存在)。
|
||||
|
||||
**与相邻机制的分工**:
|
||||
|
||||
| 机制 | 管什么 |
|
||||
|---|---|
|
||||
| §9 技能集体检 | 技能**挑不挑得中**(description 区分度) |
|
||||
| §10 技能重组 | 技能**加载后吃多少上下文 / 判据找不找得到**(文件内部结构) |
|
||||
| `session-mechanism §10.1` | 要不要把多个技能**合并** |
|
||||
| `dsh-architecture-lifecycle` | 架构**过程文档 vs 定稿**的分层 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 通用拆分器 `scripts/split_skill.py`
|
||||
|
||||
```bash
|
||||
<python> scripts/split_skill.py --spec <spec.json>
|
||||
```
|
||||
|
||||
**规格格式**(照抄改):
|
||||
|
||||
```json
|
||||
{
|
||||
"skills_dir": "E:/ProgramData/.workbuddy/skills",
|
||||
"backup_dir": "E:/ProgramData/AIProject/ai1net-dsh-server/tmp/_keep-20260922/split-bak",
|
||||
"skill": "dsh-change-workflow",
|
||||
"main_desc": "§1 六阶段流程 · §2 红线速查 · §3 Git Bash 坑",
|
||||
"moves": [
|
||||
{ "ranges": [[14, 92]],
|
||||
"ref": "00-平台速查.md",
|
||||
"title": "平台速查(硬编码事实,勿猜)",
|
||||
"covers": ["平台速查(硬编码事实,勿猜)"],
|
||||
"pointer": "> 📂 **平台速查表已下沉** → `references/00-平台速查.md`" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**要点**:
|
||||
|
||||
- `ranges` = **1-based 闭区间**,可多段(`[[481,504],[555,600]]`),**不得重叠**;一段一个 `ref`。
|
||||
- `pointer` = 插在该组区间**起点位置**的指针行(可多行用 `\n`);**要点名该档覆盖的章节**,否则违反验收 ③。
|
||||
- `covers` = 原章节名,用于生成主干的「详情档 × 覆盖的原章节 × 原行段」表(**跨档引用的定位表**)。
|
||||
- `--dry-run` 只看校验,不落盘。
|
||||
|
||||
**脚本内置自校验**(任一 FAIL 就必须停下来看,别硬推):
|
||||
|
||||
1. **行数守恒**:`主干内容行 + 详情行数 == 原行数`;
|
||||
2. **逐行包含**:`Counter(原行) <= Counter(主干非指针行 + 全部详情行)` ⇒ **丢失 0**;
|
||||
3. **节可寻址**:原文件每个 `^#{1,3} ` 标题文本都能在新结构(主干 + 任一详情档)里找到;
|
||||
4. **frontmatter**:`version:` 行唯一且值不变。
|
||||
|
||||
**幂等**:脚本永远以**备份**为拆前基准 ⇒ 反复跑不会二次切割(⚠️ 首次失误版本读的是活文件,第二次跑就 `AssertionError` —— 这个坑已修,别再写回活文件)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 2026-09-22 首跑实测数据(两个对象)
|
||||
|
||||
| 技能 | 原 SKILL.md | 主干 | 详情档 | ≤350 目标 |
|
||||
|---|---|---|---|---|
|
||||
| `dsh-change-workflow` | 1052 行 | **319 行** | 9 档 / 867 行 | ✅ 达标 |
|
||||
| `dsh-opensource-release` | 1089 行 | **534 行** | 5 档 / 629 行 | ❌ 多 184 行 |
|
||||
|
||||
- **守恒实测**:`275 + 777 = 1052`|`510 + 579 = 1089`(逐行比对丢失 0;新增的 34 / 19 行全是指针与索引行)。
|
||||
- **可寻址实测**:原 51 / 68 个标题,未命中 **0**。
|
||||
- **三处对账**:16 个文件(2 主干 + 14 详情档)三方 md5 全一致。
|
||||
|
||||
**为什么 opensource-release 到不了 350(决策记录)**:它的「不看会违规/事故」常驻项合计约 **453 行** —— 硬规则 R-O1–R-O17 **239** + 事故清单 40 + 事实 34 + 硬规则总表 23 + 用户当场纠正口径 16 + 授权结构 35 + 验证八件套 37 + SOP 主干 29。压到 350 的唯一路径是把 R-O 硬规则降级成指针 ⇒ 违背「任何会话不得放宽」。**当时的选择 = 判据常驻优先,如实报告数字**,没有为凑行数动硬规则(对应用户「确保功能效果不受影响」的硬约束)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 断头引用:怎么扫、怎么修
|
||||
|
||||
**扫**(正则挑跨节引用,逐行打印,人工过一遍):
|
||||
|
||||
```
|
||||
见上文|见下文|见前文|见后文|见上表|见下表|见上面|见下面|上文|下文|见 §|见§|参见|详见|见阶段|见坑|见附
|
||||
```
|
||||
|
||||
**实测命中 5 处**(目标全在详情档里):`见下表:dsh-univer-office`(→ 保留/移除档)· `见 §8 坑 9`、`见坑 36`(→ 实测坑档)· 详情档之间的 `见 §8 坑`、`见坑 31–36`。
|
||||
|
||||
**修法(不删字)**:主干末节加「详情档 × 覆盖的原章节 × 原行段」表 + 一句「编号引用按本表定位(下沉后原编号不再有独立章节标题)」。详情档头里也写一句同样的指引。
|
||||
⛔ **不要**去改正文里的引用句(那会破坏「逐行未改」的可校验性)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 坑(都踩过)
|
||||
|
||||
1. **必须从备份重建**:否则第二次跑读到的是已拆过的活文件,区间校验直接 `AssertionError`。
|
||||
2. **详情档编号要按文档顺序**:首跑把「浏览器验证栈」编成 `08`、而「并行调度」是 `07`(因为它先被创建)⇒ 索引表出现 `…06 / 08 / 07` 的倒序。**修法**:生成索引表时对档名 `sorted()`。
|
||||
3. **行尾只信字节级**:拆分后要确认新文件是**纯 LF**(`CR=0`);文档库 `INDEX.md` 是 CR/LF 混排(CR=5/LF=270)⇒ 改它**只做字节级单行插入**,⛔ 别整文件 Write。
|
||||
4. **登记跟改用「精确串替换器」**:每个 `old` 必须命中**恰好 1 次**,任一不中 ⇒ **整体不落盘**(比逐次 Edit 可靠、比 `sed` 安全,非 ASCII 不被改写)。
|
||||
5. **环境**:本轮本机 `bash` 包装器**整体起不来**(`ls`/`cat` 也 `command not found`、`export PATH` 救不回)⇒ 全程改走 **PowerShell + 托管 python**:脚本先 Write 成 `.py`、输出落文件再 Read。⚠️ MSYS 路径(`/e/…`)⛔ 不许交给 Windows 原生程序(会落到 `E:\e\…` 影子目录)。
|
||||
6. **对账用 Python 算 md5,别用 `md5sum`**:本机输出 `hash *path`、远端 `hash path`,多一个空格会让逐行比对假失败。
|
||||
7. **按技能名匹配表格行会误命中**:用 `NAME in line` 找登记行时,**别的技能行常在描述里提到这个技能名**(实测:README 的 `dsh-architecture-lifecycle` 行写着「与 `dsh-knowledge-upkeep` 互补」⇒ 两行同时命中,登记被挂到了错行,且我按「行里含对方名」做回退时**又同时命中两行、把两行都清了**)。
|
||||
**判据 = 只认行首单元格**:`l.startswith("| `08-skills/<n>/`")`,且命中数必须**恰好 1**;改动前后各打印一次「命中行号 + 行首 34 字」自证。
|
||||
8. **判定远端登记文件是否「只是陈旧」——别用 SequenceMatcher 的 opcode 判据**:我要求「opcode 全为 equal/delete」时,`**` 与 ` | ` 密集的长表格行会给出**假 replace** ⇒ 误报「远端含本机没有的内容」而停手(实测:README 3 行 / INDEX 4 行全被误报,实际覆盖率 1.000)。
|
||||
**正确判据 = 整行子序列覆盖率**:`sum(b.size for b in SequenceMatcher(None, 远端行, 本机行).get_matching_blocks()) / len(远端行) >= 0.98`(远端只被删减,本机只做插入)⇒ 可安全推。**更省事的 oracle = git**:`git show HEAD:<f>` 与远端比 —— 相等即"远端 = 已提交基线,差异全是未提交改动"。
|
||||
9. **`/opt/dsh/docs/` 根也镜像文档仓根**:`README.md`(技能登记表)与 `INDEX.md`(清单与状态)在服务器上各有一份 ⇒ **三处同步不能只算 `08-skills/`**。实测 2026-09-22:skills 16 文件三方 md5 全一致,而根 README/INDEX **落后一整轮登记**(差异恰为被改的 3 + 4 行)。推送后 `chmod 600` + `chown root:root`,旧副本备份到 `/opt/dsh/backups/docs-root/`。
|
||||
@@ -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