tools/skillgen:技能檔的產生器

SKILL.md 不是手寫的——是從 fragments 渲染出來的,還有防 drift 校驗
檔案:tools/skillgen/(gen.py 54KB · platforms.toml 12KB · fragments/ · expected/)

大方向

這是「技能檔結構」的生產端。核心哲學(docstring 一開頭就講):

Fragments under tools/skillgen/fragments/ are the single source of truth a human edits; the files under graphify/skill*.md and graphify/skills/<platform>/references/ are generated, committed artifacts.

也就是:人只編輯 fragments,機器渲染出所有平台的 skill 成品。render 是冪等的(固定順序、按名排序、LF 換行、產出檔不含時間戳),並用 expected/ 當 baseline 擋住 drift。

使用方式(docstring)

python -m tools.skillgen                  # 重新產生所有平台的 artifacts
python -m tools.skillgen --platform claude
python -m tools.skillgen --check          # byte-diff 渲染 vs 已提交 + expected/,有 drift 就 exit 1
python -m tools.skillgen --audit-coverage # 每個 host:驗證該 host 的 v8 body 每節都住在自己渲染裡
python -m tools.skillgen --schema-singleton # 驗證 file_type enum 到處 byte-identical
python -m tools.skillgen --monolith-roundtrip # 驗證每個 monolith == v8 modulo enum
python -m tools.skillgen --always-on-roundtrip # 驗證每個 always_on/*.md 重現其舊常數
python -m tools.skillgen --bless          # 用目前渲染重寫 expected/

關鍵函數

class Platform · load_platforms() gen.py:265 / 301platforms.toml 的載入與型別化。

platforms.toml 宣告每個 host 怎麼組裝 skill:bucket(split 或 monolith)、core(split 用的 core 模板)、skill_dst / refs_dst(輸出位置)、name / description(frontmatter,description 各平台 verbatim 保留)、dispatch(Part-B dispatch fragment)、extraction(verbose/compact,選 extraction-spec 的內容)、shell(posix/powershell)、claude_mdhooks_variantextra_sectionsmonolith

load_platforms()tomllib 解析(Python 3.10 退回 tomli),每個 [platform.<key>] table 變成一個 Platform 物件。

render(platform) · _render_core · _render_frontmatter gen.py:423 / 360 / 347把 fragments 組裝成該平台的 SKILL.md 與 references。

渲染邏輯:

  • _render_frontmatter(platform) — 產生 ---\nname: …\ndescription: …\n---
  • _render_core(platform) — 載入 fragments/core/core.md(split 的 lean core 模板),把 platform 的 slot 填進去:dispatch fragment、shell fragment、hooks pointer、extra_sections、reference index。
  • render(platform) — 回傳 list[RenderedArtifact](SKILL.md + references/ 側車的渲染結果)。
  • references 是「自動組裝」的——platform 只宣告自己的 deltas,共同的(query、extraction-spec、hooks、add-watch…)自動從 fragments/references/shared/ 帶上。

這解釋了為什麼 17 個平台的 skill-<platform>.md 有 40KB 卻彼此只差一點點——它們共用一個 core 模板。

check(artifacts) · bless(artifacts) gen.py:525 / 514drift 守門員:check 驗證、bless 重寫 baseline。

防 drift 的雙向機制:

  • check() — 把渲染結果與已提交的檔案做 byte-diff,再與 expected/ 做 byte-diff。任何一個不一致就回報(CI 會 exit 1)。這確保「手改了 generated artifact」或「fragment 改了但沒重新產生」都會被抓到。
  • bless() — 把目前的渲染結果寫進 expected/_expected_path 命名 graphify__skill.md 等)——「這是我承認的真相」。改模板時先 bless 再 commit。
audit_coverage · schema_singleton · *_roundtrip gen.py:610 / 695 / …更嚴格的防護:內容覆蓋、enum 單一、monolith round-trip。

段落式說明(skillgen 的「驗證套組」,都是為了讓「拆分 platform」這件事安全):

  • audit_coverage(platform) — 用 _V8_BASELINE_SHAgit show 拉 v8 分支的原始 skill body)逐 host 驗證:該 host 自己的 v8 body 每個 heading 都「單一居住」在它自己的渲染裡——一個只影響單一 host 的漏接才看得見。
  • schema_singleton(platforms) — 驗證 file_type enum 在所有 artifact 裡 byte-identical。
  • monolith_roundtrip / always_on_roundtrip — 驗證 monolith skill 與 always_on 檔能重現過去的常數。
_is_*_fix_line 家族 gen.py:713-916修復檔案的「已知正確」識別器。

段落式說明:這組 _is_chunk_cleanup_fix_line_is_zero_node_guard_fix_line_is_no_api_key_fix_line 等函數,是為「語意修復」判定的——渲染引擎產生內容時,需要識別哪些行屬於特定的歷史修復(例如 zero-node guard、no-API-key 提示),以便穩定地定位/合併。每個都是一個小 predicate,名稱本身就描述了它認得哪一行。

看完這頁你應該能說出:fragments 與 artifacts 的關係、check 與 bless 的差別、為什麼 skill 檔不能直接手改。回到 技能檔結構 再看一次,整個「生成 → 安裝 → 載入」循環就通了。

下一頁:scripts/gen_demo_path.py——README 首圖 SVG 動畫的生成器。

① 進階真實情境 Worked Example:用 skillgen 管理「自家多平台 skill」的版本升級

前面是「看懂 render/check/bless」;這次是把它當版本管理工具:你的公司 fork 了 graphify,改了一堆 fragments(加了內部工具指引),現在上游釋出新版,你要安全升級。

  1. 凍結現況:先 python -m tools.skillgen --check 確認 fork 的產物與 expected/ 一致(drift 歸零才談升級)。
  2. 拉上游、對帳 diff:merge 上游的 fragments/ 變更——用 --audit-coverage 驗證「每個 host 的 v8 body 每個 heading 仍單一居住在自己的渲染裡」,抓到「上游改了某段、卻漏接進你 fork」的情形。
  3. 衝突處理:上游改了「你改過」的 fragment → 人工三方合併(上游版 + 你的版 + 共同基底),改回 fragments 而非產物。
  4. 重渲染 + 驗證python -m tools.skillgen 重產所有平台 → --check 過 → --schema-singleton 確認 file_type enum 全站 byte-identical → --monolith-roundtrip 確認 monolith 重現。
  5. commit 三件套:fragments + 產物 + expected/ 一起 commit(baseline 更新)。
為什麼選這條路徑:skill 檔是 generated artifacts,若直接改 SKILL.md,上游每次升級都會與你的修改衝突、且無法稽核。從 fragments 走的升級是「三方合併一次、重渲染全部、驗證套組兜底」——audit/roundtrip 那組驗證讓「拆分 17 個平台」這件事安全到可以被 CI 執行。

② 深入原理擴充:check vs bless vs audit——三種「防 drift」的層次

skillgen 的防護常被混為一談,其實是三個不同層次:

大家以為建圖正確、但其實有誤的案例:以為「--bless 通過 = 產物正確」。bless 只代表「目前渲染與我承認的 baseline 一致」——它完全不知道「渲染邏輯本身有沒有 bug」。常見誤用:某個 fragment 放錯位置(例如把 claude 專屬的 section 寫進 shared/),render 照樣輸出、check 照樣過、bless 照樣接受——直到 --audit-coverage 或人工發現「claude 的 SKILL.md 混進了 codex 的內容」。bless 前先跑 audit/roundtrip,不要只靠 check。

③ 診斷式疑難排解表

症狀可能原因解決方案
--check 一直 exit 1產物被手改、或 fragment 改了沒重產python -m tools.skillgen 重產;確認後 --check 過;不要手改產物
bless 後 --check 仍失敗expected/ 的命名路徑與實際產物路徑不一致對照 _expected_path 命名(graphify__skill.md 等);確認 bless 寫對位置
某平台內容缺一段(check 卻過)該段被放在不屬於它的 fragment 或 shared/ 漏接--audit-coverage;逐 heading 對照 v8 baseline 找「漏接」
description 各平台不同步某平台覆寫了 description(platforms.toml 設了不同值)確認 platforms.toml 的 description 為「PRESERVED VERBATIM」;統一單一來源
新增平台後部分驗證炸掉新平台的 hook_variant/shell 組合沒被 roundtrip 覆蓋為新平台補 monolith/always_on roundtrip;確認 expected/ 有該平台 baseline

④ 進階挑戰題

  1. audit_coverage 用「heading 單一居住」驗證內容歸位。請設計一個反例:一個 heading 內容「分成兩半、各住不同 host 的渲染」——audit 能抓到嗎?若要抓,需要什麼更強的驗證?
  2. 「bless 前先跑 audit/roundtrip」。請設計一個 CI gate 流程,把 check/audit/roundtrip/bless 的執行順序與人工確認點排成一個可操作的 PR 檢查表。
  3. fragments 是「單一來源」,但 17 個平台仍可能各有 shell/hook 差異。如果「同一個 fragment 的文字」在不同平台該有微妙的用字差異(例如 UK/US 英文),你會引入「條件式 fragment」還是「多來源 + 合併」?分析兩者的維護成本。