scenarios.py 4.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """场景级注册表 纯函数 (① 体检 workflow 升格; 批量装配 SOP §5)。
  2. 消费 configs/scenario_registry.yaml。答: 给定商业场合(scenario) →
  3. 出哪些报告章 (chapter_subset) · 必产哪些交付域 (required_domains) ·
  4. 什么审级 (review_tier, 走 SOP §6.1 RV-1) · 什么裁决基调 (flavor)。
  5. 编号 namespace (SOP §4.-1): 域=交付九域 D1-D9; 章=报告7章; 审级 RV-1。
  6. 纯函数 (coverage 扫描只读文件, 无写副作用)。机器位 CLI = scripts/run_scenario.py。
  7. """
  8. try:
  9. from app_common.app_common_guanlan.api import install_root as _install_root
  10. except ImportError: # 理论不可达;包结构异常时回退到按位置上跳
  11. from pathlib import Path as _P
  12. def _install_root(_f): return _P(_f).resolve().parents[4]
  13. from src import paths as P
  14. import os
  15. from pathlib import Path
  16. import yaml
  17. REPO = _install_root(__file__)
  18. REGISTRY = P.config("scenario_registry.yaml") # 配置唯一取用口 (src/paths.py)
  19. # 交付九域 → 证据关键词 (扫 outputs/<farm>/sop/ 文件名判该域是否已产)。SOP §4.-1 crosswalk。
  20. DOMAIN_EVIDENCE = {
  21. "D1": ["风资源", "风况", "wind_resource"],
  22. "D2": ["功率", "power", "mast", "偏航", "yaw", "变桨", "pitch", "回桨", "气动", "aero", "达成"],
  23. "D3": ["温度", "temp", "振动", "vib", "轴承", "低速轴", "主轴", "1P", "探头"],
  24. "D4": ["故障", "停机", "fault", "pareto", "帕累托", "事件"],
  25. "D5": ["可利用率", "可靠", "mtbf", "avail", "复发"],
  26. "D6": ["安全", "前兆", "safety", "安全链"],
  27. "D7": ["运维", "备件", "检修", "承包商"],
  28. "D8": ["寿命", "rul", "延寿", "役龄"],
  29. "D9": ["方案", "隐患清单", "隐患排查", "治理", "体检报告", "报告"],
  30. # 前置/横切 (非交付域, 但 workflow 前置门必查)
  31. "限电": ["限电", "curtail", "弃风"],
  32. "损失": ["损失", "电量", "waterfall", "瀑布"],
  33. }
  34. VALID_DOMAINS = {f"D{i}" for i in range(1, 10)}
  35. TIER_ORDER = {"lite": 0, "双模型": 1, "AdvCI": 2}
  36. def load_registry(path=REGISTRY):
  37. with open(path, encoding="utf-8") as f:
  38. return yaml.safe_load(f)
  39. def resolve(scenario_id, reg=None):
  40. """按 id 或 alias 找场景 dict; 未知 → KeyError。"""
  41. reg = reg or load_registry()
  42. for s in reg["scenarios"]:
  43. if s["id"] == scenario_id or scenario_id in (s.get("aliases") or []):
  44. return s
  45. raise KeyError(f"未知场景 {scenario_id!r} (合法: {[s['id'] for s in reg['scenarios']]})")
  46. def required_domains(scenario_id, reg=None):
  47. s = resolve(scenario_id, reg)
  48. return list(s.get("required_domains") or s.get("domain_weight", {}).get("主攻", []))
  49. def chapter_subset(scenario_id, reg=None):
  50. return list(resolve(scenario_id, reg).get("chapter_subset") or [])
  51. def review_tier(*, has_economic, has_confirmed, systemic, destination):
  52. """SOP §6.1 RV-1 审级门 (唯一正本逻辑; 场景 default 仅 doc 提示, 实际 tier 由此算)。
  53. destination ∈ {内部样板, 业主实名, 上云公开}。返回 (tier, 理由)。"""
  54. if destination == "上云公开" or has_economic or has_confirmed:
  55. why = []
  56. if destination == "上云公开": why.append("上云/公开")
  57. if has_economic: why.append("含经济数字¥")
  58. if has_confirmed: why.append("确诊异常")
  59. return "AdvCI", "RV-1: " + "∨".join(why) + " → AdvCI-PR (独立 C/D/R subagent)"
  60. if systemic:
  61. return "双模型", "RV-1: 跨机系统性(≥3台同机制) → 双模型审 (Gemini+DeepSeek)"
  62. return "lite", "RV-1: 健康清场/无确诊无¥ → lite (1 独立 subagent, 禁主 Claude 自审)"
  63. def check_coverage(farm, scenario_id, reg=None, sop_dir=None):
  64. """扫 outputs/<farm>/sop/ 文件名, 判每个 required_domain 是否已产。
  65. 返回 {domain: {"covered": bool, "evidence": [文件名...]}} + 前置门(限电/损失)。
  66. coverage = §0.8 required_outputs / 九域矩阵'缺一中止'的机器化 (缺 = 显式声明, 非静默跳过)。"""
  67. sop_dir = Path(sop_dir) if sop_dir else P.sop(farm)
  68. files = [p.name for p in sop_dir.iterdir()] if sop_dir.is_dir() else []
  69. def hits(keys):
  70. return [fn for fn in files if any(k.lower() in fn.lower() for k in keys)]
  71. req = required_domains(scenario_id, reg)
  72. out = {}
  73. for d in req + ["限电", "损失"]:
  74. ev = hits(DOMAIN_EVIDENCE.get(d, []))
  75. out[d] = {"covered": bool(ev), "evidence": ev}
  76. return out