Files
playbook/cangjie-skill/scripts/cangjie.py
T
2026-08-31 05:00:46 +08:00

322 lines
13 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
"""cangjie.py — 仓颉统一薄 CLI(路线 C:只做编排与确定性操作,蒸馏仍由 Agent 完成)。
子命令:
doctor 环境自检(无网络、无重依赖场景可通过核心检查)
migrate-legacy 旧 one-to-one pack + 人工能力映射 → Capability Bundle.cangjie/capabilities/
compile Capability Bundle → single | compact packauto 决策 + 锁 + staging + 原子发布)
replan-output 重新评估输出策略,只生成 side-by-side 预览,不迁移
update 登记新来源 → diff → change-set → 影响分析 → 生成待处理 Agent 任务(不自动改 Skill
repair 校验失败案例 → 快照 → 生成诊断任务(语义修复由 Agent 完成后回归)
rollback 恢复最近快照(--list 查看)
eval 分发到触发评测(默认)或输出评测
benchmark 分发到 benchmark.py
`compile` 的输入必须是已验证 Capability Bundle;本 CLI 不声称能从原始书籍一键蒸馏。
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from cangjie_common import ( # noqa: E402
TOOL_VERSION,
WriterLock,
atomic_publish,
create_run_workdir,
dump_json,
load_yaml,
new_run_id,
sha256_file,
snapshot_dir,
split_frontmatter,
)
SCRIPTS = Path(__file__).parent
# ---------- doctor ----------
def cmd_doctor(_args) -> int:
ok = True
print(f"{TOOL_VERSION}\npython: {sys.version.split()[0]}")
for mod, required in (("yaml", True), ("tiktoken", False), ("jsonschema", False)):
try:
__import__(mod)
print(f" [ok] {mod}")
except ImportError:
level = "缺失(必需)" if required else "缺失(可选)"
print(f" [{'FAIL' if required else 'warn'}] {mod} {level}")
ok = ok and not required
for rel in ("capability-bundle.schema.json", "registry-entry.schema.json", "contracts/source-document.schema.json"):
p = SCRIPTS.parent / "schemas" / rel
print(f" [{'ok' if p.exists() else 'FAIL'}] schemas/{rel}")
ok = ok and p.exists()
print("doctor:", "PASS" if ok else "FAIL")
return 0 if ok else 1
# ---------- migrate-legacy ----------
ROUTER_GATE = {"independent_intent": False, "independent_contract": True, "independent_run": True,
"independent_reuse": False, "independent_eval": True}
PROMOTED_GATE = {k: True for k in ROUTER_GATE}
def cmd_migrate_legacy(args) -> int:
"""人工能力映射 + 旧 pack → Capability Bundle。语义字段全部来自映射,不猜测。"""
import yaml as _yaml
m = load_yaml(Path(args.map))
pack = Path(args.pack)
bundle_dir = pack / ".cangjie" / "capabilities"
(bundle_dir / "cards").mkdir(parents=True, exist_ok=True)
(bundle_dir / "book").mkdir(exist_ok=True)
capabilities = []
for cap in m["capabilities"]:
src_md = pack / cap["slug"] / "SKILL.md"
fm, body = split_frontmatter(src_md.read_text(encoding="utf-8"))
dest = cap.get("pack_destination", "router")
header = (
f"<!-- capability_id: {cap['id']} | revision: 1 | status: active -->\n"
f"<!-- 来源: {m['book']['source_pack']}/{cap['slug']}/SKILL.md(正文逐字保留) -->\n\n"
)
(bundle_dir / "cards" / f"{cap['slug']}.md").write_text(header + body, encoding="utf-8")
capabilities.append({
"capability_id": cap["id"],
"revision": 1,
"status": "active",
"aliases": [],
"supersedes": [],
"merged_into": None,
"split_into": [],
"slug": cap["slug"],
"title": cap["title"],
"importance": cap["importance"],
"importance_rationale": cap["importance_rationale"],
"one_liner": cap["one_liner"],
"intents": cap["intents"],
"keywords": cap["keywords"],
"also_read": cap.get("also_read", []),
"card": f"cards/{cap['slug']}.md",
"frontmatter": {"description": str(fm.get("description", "")).strip(),
"tags": fm.get("tags", [])},
"source_evidence": [{"source_id": "src-main-book", "location": str(fm.get("source_chapter", ""))}],
"promotion": {"destination": dest,
"gate": PROMOTED_GATE if dest == "promoted" else dict(ROUTER_GATE),
"notes": cap.get("promotion_notes", "")},
})
bundle = {
"schema_version": 1,
"bundle_id": f"bundle.{m['entry']['name']}",
"book": m["book"],
"entry": m["entry"],
"router_entry": m["router_entry"],
"promotion_budget": m.get("promotion_budget", 8),
"capabilities": capabilities,
}
(bundle_dir / "verified.yaml").write_text(
_yaml.safe_dump(bundle, allow_unicode=True, sort_keys=False, width=120), encoding="utf-8")
dump_json(bundle_dir / "destinations.json", {
"bundle_id": bundle["bundle_id"],
"destinations": {c["capability_id"]: ({"promoted_to": c["slug"]} if c["promotion"]["destination"] == "promoted"
else {"served_by": m["router_entry"]["name"]})
for c in capabilities},
})
for src_name, dst_name in (("BOOK_OVERVIEW.md", "overview.md"), ("GLOSSARY.md", "glossary.md")):
src = pack / src_name
if src.exists():
shutil.copyfile(src, bundle_dir / "book" / dst_name)
print(f"Capability Bundle 已生成: {bundle_dir}{len(capabilities)} 个能力)")
return 0
# ---------- compile ----------
def cmd_compile(args) -> int:
from compile_pack import compile_pack_tree
from compile_single import build_tree, write_tree
from select_output_strategy import decide, render_report
bundle_dir = Path(args.bundle)
sidecar = bundle_dir.parent if bundle_dir.name == "capabilities" else bundle_dir
run_id = new_run_id()
workdir = create_run_workdir(sidecar, run_id)
decision = decide(bundle_dir, args.output, args.purpose)
dump_json(workdir / "output-decision.json", decision)
(workdir / "output-decision.md").write_text(render_report(decision, bundle_dir), encoding="utf-8")
print(render_report(decision, bundle_dir))
if args.output == "auto" and not args.yes:
print("auto 模式需要用户轻确认;确认后请加 --yes 重新运行(按推荐),或显式 --output single|pack。")
return 2
selected = decision["selected"]
files = build_tree(bundle_dir, "single") if selected == "single" else compile_pack_tree(
bundle_dir, allow_over_budget=args.allow_over_budget)
target = Path(args.out)
staging = target.parent / f".staging-{run_id}-{target.name}"
if staging.exists():
shutil.rmtree(staging)
write_tree(files, staging)
check = subprocess.run([sys.executable, str(SCRIPTS / "validate_skill_pack.py"), str(staging)],
capture_output=True, text=True)
(workdir / "staging-validation.log").write_text(check.stdout + check.stderr, encoding="utf-8")
if check.returncode != 0:
print(check.stdout + check.stderr)
raise SystemExit("[hard-gate] staging 校验未通过,已保留 staging 供排查,不发布")
with WriterLock(target):
if target.exists():
snap = snapshot_dir(target, sidecar / "snapshots", f"pre-{run_id}")
print(f"已快照旧版本: {snap}")
atomic_publish(staging, target, {
"bundle_id": load_yaml(bundle_dir / "verified.yaml")["bundle_id"],
"bundle_sha256": sha256_file(bundle_dir / "verified.yaml"),
"variant": selected,
"run_id": run_id,
"decision": decision,
}, allow_overwrite_edits=args.force_overwrite)
print(f"已原子发布 {selected} 产物到 {target}run: {run_id}")
return 0
# ---------- replan-output ----------
def cmd_replan_output(args) -> int:
from select_output_strategy import decide, render_report
bundle_dir = Path(args.pack) / ".cangjie" / "capabilities"
decision = decide(bundle_dir, "auto", args.purpose)
print(render_report(decision, bundle_dir))
print("(dry-run 预览:未做任何迁移;确认变更请显式运行 compile 并选择新的 --output")
return 0
# ---------- update / repair / rollback / eval / benchmark ----------
def cmd_update(args) -> int:
from update_flow import run_update # Phase 2B
return run_update(Path(args.pack), Path(args.add))
def cmd_repair(args) -> int:
from repair_flow import run_repair # Phase 3
return run_repair(Path(args.pack), Path(args.case))
def cmd_rollback(args) -> int:
sidecar = Path(args.pack) / ".cangjie"
snaps = sorted((sidecar / "snapshots").glob("*")) if (sidecar / "snapshots").exists() else []
if args.list or not args.to:
print("可用快照:" + ("\n " + "\n ".join(s.name for s in snaps) if snaps else " (无)"))
return 0
snap = sidecar / "snapshots" / args.to
if not snap.is_dir():
raise SystemExit(f"快照不存在: {snap}")
target = Path(args.target)
with WriterLock(target):
if target.exists():
snapshot_dir(target, sidecar / "snapshots", "pre-rollback")
shutil.rmtree(target)
shutil.copytree(snap, target)
print(f"已回滚 {target}{snap.name}")
return 0
def _dispatch(script: str, extra: list[str]) -> int:
return subprocess.call([sys.executable, str(SCRIPTS / script), *extra])
def main() -> int:
# 这两个命令需要把未知选项原样透传给子工具。argparse 的子解析器
# 会在 `--token-config` 这类选项上提前报错,因此在根解析前直接分发。
if len(sys.argv) > 1 and sys.argv[1] == "benchmark":
return _dispatch("benchmark.py", sys.argv[2:])
if len(sys.argv) > 1 and sys.argv[1] == "eval":
extra = sys.argv[2:]
script = "run_trigger_evals.py"
if extra and extra[0] in {"trigger", "output"}:
script = "run_output_evals.py" if extra[0] == "output" else script
extra = extra[1:]
return _dispatch(script, extra)
ap = argparse.ArgumentParser(prog="cangjie", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("doctor")
p = sub.add_parser("migrate-legacy")
p.add_argument("--map", required=True, help="人工能力映射 capability-map.yaml")
p.add_argument("--pack", required=True, help="旧 one-to-one pack 目录")
p = sub.add_parser("compile")
p.add_argument("--bundle", required=True)
p.add_argument("--out", required=True)
p.add_argument("--output", choices=["auto", "single", "pack"], default="auto")
p.add_argument("--purpose", choices=["learning", "reference", "workflow", "distribution"], default=None)
p.add_argument("--yes", action="store_true", help="auto 模式下按推荐执行(轻确认)")
p.add_argument("--allow-over-budget", action="store_true")
p.add_argument("--force-overwrite", action="store_true", help="丢弃已发布目录中的本地手工修改")
p = sub.add_parser("replan-output")
p.add_argument("--pack", required=True)
p.add_argument("--purpose", choices=["learning", "reference", "workflow", "distribution"], default=None)
p.add_argument("--dry-run", action="store_true", default=True)
p = sub.add_parser("update")
p.add_argument("--pack", required=True)
p.add_argument("--add", required=True, help="新增来源文件(Markdown/TXT")
p = sub.add_parser("repair")
p.add_argument("--pack", required=True)
p.add_argument("--case", required=True, help="failure-case YAMLfailure-case.schema.json")
p = sub.add_parser("rollback")
p.add_argument("--pack", required=True)
p.add_argument("--target", help="要恢复的已发布目录")
p.add_argument("--to", default=None)
p.add_argument("--list", action="store_true")
for name, script in (("eval", "run_trigger_evals.py"), ("benchmark", "benchmark.py")):
p = sub.add_parser(name, add_help=False)
p.add_argument("extra", nargs=argparse.REMAINDER)
p.set_defaults(script=script)
args = ap.parse_args()
if args.cmd == "doctor":
return cmd_doctor(args)
if args.cmd == "migrate-legacy":
return cmd_migrate_legacy(args)
if args.cmd == "compile":
return cmd_compile(args)
if args.cmd == "replan-output":
return cmd_replan_output(args)
if args.cmd == "update":
return cmd_update(args)
if args.cmd == "repair":
return cmd_repair(args)
if args.cmd == "rollback":
return cmd_rollback(args)
return _dispatch(args.script, args.extra)
if __name__ == "__main__":
raise SystemExit(main())