# ============================================================ # 方案生成 · Explore 沙盒多策略对比(moduleId: domain-scenario, 可重生 ✅) # plan.md §9.7 + §5.1:一次"对比"在深拷贝沙盒里并行试排多个策略, # 永不触碰主干世界状态(黄金测试 test_m2_state 固化此不变式); # 产出方案卡组(ScenarioCard 的 M2 子集),计划员"看验收选方案"。 # ============================================================ from __future__ import annotations # 前向类型引用 import copy # 沙盒深拷贝 import uuid # 方案/块 ID from typing import Any # 类型标注 from server.contracts import ScheduleResult, UIAction, UIBlock # 契约 from server.engines import get_engine # 引擎工厂 from server.engines.base import EngineParams # 引擎入参 from server.timeutil import add_minutes, fmt_date, today0 # 日期工具 # 世界状态类型别名 World = dict[str, Any] # 对比的策略组合(§9.7 硬规则 2:策略维度多样化,不凑数) _STRATEGIES: list[tuple[str, str]] = [ ("DELIVERY_FIRST", "交期优先"), # EDD 内核:保交期 ("CAPACITY_BALANCE", "产能均衡"), # 均衡负荷:保产线 ("COMPREHENSIVE", "综合优化"), # 默认权衡 ] def _sandbox_counter(): """沙盒专用发号器:与主干 WorldStore 计数器完全隔离(防串号)。""" counters: dict[str, int] = {} # 沙盒内独立计数 def next_id(kind: str) -> int: # 闭包发号 counters[kind] = counters.get(kind, 0) + 1000000 # 加大基数以便肉眼区分沙盒对象 return counters[kind] # 返回号 return next_id # 返回闭包 def compare_scenarios(world: World, engine_type: str = "RULE") -> tuple[str, UIBlock]: """在沙盒中并行试排多策略并产出方案卡组(权力等级 P1:只读主干 + 写沙盒)。 Args: world: 主干世界状态(只读,函数内部深拷贝) engine_type: 引擎类型(M1/M2 由 RULE 承接) Returns: (回复文案, scenario-cards UI 块) """ baseline = world["scheduleVersions"][-1] if world["scheduleVersions"] else None # 基准=当前最新版本(diff 对照) start = fmt_date(add_minutes(today0(), 24 * 60)) # 统一起排日:明天(可比性) cards: list[dict[str, Any]] = [] # 方案卡集合 for strategy, label in _STRATEGIES: # 逐策略沙盒试排 sandbox = copy.deepcopy(world) # Explore 沙盒:深拷贝隔离(§5.1 铁律) params = EngineParams(orderIds=[], engineType=engine_type, strategyTemplate=strategy, planningHorizonDays=14, startDate=start) # 试排参数 result: ScheduleResult = get_engine(engine_type).solve(sandbox, params, _sandbox_counter()) # 沙盒求解 card = { # 方案卡(ScenarioCard 的 M2 子集) "scenarioId": uuid.uuid4().hex[:8], # 方案 ID(M2:方案即"可采用的策略",M3 起挂真分支) "label": label, # 中文名 "strategy": strategy, # 策略模板(采用时重跑用——引擎确定性保证结果一致) "engine": engine_type, # 引擎类型 "kpi": { # KPI 组(对比维度) "poCount": result.poCount, "woCount": result.woCount, "conflictCount": result.conflictCount, "totalTardiness": round(result.totalTardiness, 1), "avgUtilization": round(result.avgUtilization, 3), "totalCost": round(result.totalCost), }, # 相对基准版本的差异摘要(§9.7 diff_vs_baseline 的 M2 简化) "diffVsBaseline": ({ "tardiness": round(result.totalTardiness - baseline["totalTardiness"], 1), "conflicts": result.conflictCount - baseline["conflictCount"], "utilization": round(result.avgUtilization - baseline["avgUtilization"], 3), } if baseline else None), # 风险提示:从沙盒冲突里提炼要点(最多 2 条) "risks": [c["description"] for c in sandbox["conflicts"][-result.conflictCount:][:2]] if result.conflictCount else [], } cards.append(card) # 收集方案卡 # 组装方案卡组 UI 块(§6.2 scenario-cards 类型;每卡带"采用"动作 P1) block = UIBlock( blockId=f"scenarios-{uuid.uuid4().hex[:8]}", # 块 ID type="scenario-cards", # 块类型 props={"cards": cards, # 卡组数据 "baseline": baseline["versionNo"] if baseline else None}, # 基准版本号(对照展示) actions=[UIAction(actionId="scenario.apply", label="采用此方案", power="P1", # 采用动作(前端逐卡渲染) payload={})], ) # 回复文案:一句话对比结论(找延迟最小者作为推荐) best = min(cards, key=lambda c: (c["kpi"]["totalTardiness"], c["kpi"]["conflictCount"])) # 简单推荐规则 text = (f"已在沙盒中并行试排 {len(cards)} 种策略(未影响当前方案):\n" + "\n".join(f"· {c['label']}:延迟 {c['kpi']['totalTardiness']}h / 冲突 {c['kpi']['conflictCount']} / " f"利用率 {round(c['kpi']['avgUtilization'] * 100)}%" for c in cards) + f"\n综合看【{best['label']}】表现最好。点方案卡上的“采用此方案”即可正式排产。") # 推荐话术 return text, block # 返回文案与卡组块