#!/usr/bin/env python3 """select_output_strategy.py — auto 输出决策器(方案 §4.6.5,策略 single-first-v1)。 规则(首版): 1. 用户明确目的(--purpose learning|reference|workflow|distribution)优先; 2. 未明确目的时:至少 3 个能力通过晋级门、且晋级门触发验证达到预注册阈值(TBD-after-baseline, 基线前该分支不启用)→ 推荐 pack; 3. 其他情况一律推荐 single(single-first)。 输出可解释 decision report(output-decision.schema.json)。默认值只是推荐,不能取消用户显式选择。 """ from __future__ import annotations import argparse import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from cangjie_common import dump_json, load_yaml # noqa: E402 from compile_single import active_capabilities, promoted_capabilities # noqa: E402 POLICY = "single-first-v1" PURPOSE_TO_MODE = { "learning": "single", "reference": "single", "workflow": "pack", "distribution": "pack", } def decide(bundle_dir: Path, requested: str, purpose: str | None, *, trigger_validation_ready: bool = False) -> dict: bundle = load_yaml(bundle_dir / "verified.yaml") caps = active_capabilities(bundle) promoted = promoted_capabilities(bundle) budget = int(bundle.get("promotion_budget", 8)) reasons: list[str] = [] if requested in ("single", "pack"): selected = requested reasons.append(f"用户显式选择 {requested},auto 决策器不覆盖用户选择") elif purpose: selected = PURPOSE_TO_MODE[purpose] reasons.append(f"用户目的为 {purpose} → {selected}") elif len(promoted) >= 3 and trigger_validation_ready: selected = "pack" reasons.append(f"{len(promoted)} 个能力通过晋级门(>=3)且触发验证达到预注册阈值") else: selected = "single" if len(promoted) >= 3: reasons.append( f"{len(promoted)} 个能力通过晋级门,但触发验证阈值为 TBD-after-baseline 尚未启用;" "按 single-first 原则先推荐 single,拆分由使用证据驱动" ) else: reasons.append(f"仅 {len(promoted)} 个能力通过晋级门(<3),不满足 pack 推荐条件") alternative = ( f"compact pack(1 个来源路由入口 + {len(promoted)} 个晋级 Skill,共 {1 + len(promoted)} 个可发现入口)" if selected == "single" else f"single(1 个入口 + {len(caps)} 张内部能力卡)" ) return { "schema_version": 1, "requested": requested, "selected": selected, "decision_policy": POLICY, "skill_budget": budget, "promoted_count": len(promoted), "capability_count": len(caps), "reasons": reasons, "alternative": alternative, "user_confirmed": requested != "auto", "preserve_strategy_on_update": True, } def render_report(decision: dict, bundle_dir: Path) -> str: bundle = load_yaml(bundle_dir / "verified.yaml") return f"""# 输出策略决策报告 推荐:**{decision['selected']}**(requested: {decision['requested']},策略 {decision['decision_policy']}) 理由: {chr(10).join(f'- {r}' for r in decision['reasons'])} 产物:{'1 个 Skill + ' + str(decision['capability_count']) + ' 张内部能力卡 + 章节/术语/速查 references' if decision['selected'] == 'single' else f"1 个来源路由入口 + {decision['promoted_count']} 个晋级 Skill(共 {1 + decision['promoted_count']} 个可发现入口)"} 备选:{decision['alternative']} > 来源:{bundle['book']['title']}({decision['capability_count']} 个 active 能力,晋级预算 {decision['skill_budget']}) > 请回答:按推荐 / 改成 single / 改成 pack。默认值只是推荐,不会取消你的显式选择。 """ def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--bundle", required=True) ap.add_argument("--requested", choices=["auto", "single", "pack"], default="auto") ap.add_argument("--purpose", choices=list(PURPOSE_TO_MODE), default=None) ap.add_argument("--out-json", default=None) args = ap.parse_args() decision = decide(Path(args.bundle), args.requested, args.purpose) print(render_report(decision, Path(args.bundle))) if args.out_json: dump_json(Path(args.out_json), decision) return 0 if __name__ == "__main__": raise SystemExit(main())