#!/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 pack(auto 决策 + 锁 + 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"\n" f"\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 YAML(failure-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())