"""Simulation engine for AI PMO decision runtime.""" from __future__ import annotations from .shared import ( CanonicalIntent, ROUTE_PROJECT_SIGNAL_CANCEL, ROUTE_PROJECT_SIGNAL_COMMIT, ROUTE_PROJECT_SIGNAL_DISCOVER, ROUTE_PROJECT_SIGNAL_REFRESH, Any, normalize_workload_ratio, ) class SimulationEngine: """执行前仿真。""" BUDGET_COST_BY_OPERATION = { "query.answer": 1.0, "project.create": 120.0, "task.create": 14.0, "signal.enter": 8.0, "signal_source.onboard": 40.0, ROUTE_PROJECT_SIGNAL_DISCOVER: 3.0, ROUTE_PROJECT_SIGNAL_COMMIT: 8.0, ROUTE_PROJECT_SIGNAL_REFRESH: 3.0, ROUTE_PROJECT_SIGNAL_CANCEL: 0.2, } HOURS_BY_OPERATION = { "query.answer": 0.2, "project.create": 12.0, "task.create": 2.5, "signal.enter": 1.5, "signal_source.onboard": 6.0, ROUTE_PROJECT_SIGNAL_DISCOVER: 0.8, ROUTE_PROJECT_SIGNAL_COMMIT: 1.5, ROUTE_PROJECT_SIGNAL_REFRESH: 0.8, ROUTE_PROJECT_SIGNAL_CANCEL: 0.1, } @classmethod def simulate(cls, *, intent: CanonicalIntent, world_model: dict[str, Any], execution_plan: dict[str, Any]) -> dict[str, Any]: actions = execution_plan.get("actions") if isinstance(execution_plan.get("actions"), list) else [] operation_codes = [str(item.get("operation_code") or "").strip() for item in actions if isinstance(item, dict)] estimated_budget_delta = sum(cls.BUDGET_COST_BY_OPERATION.get(code, 0.0) for code in operation_codes) estimated_capacity_delta = sum(cls.HOURS_BY_OPERATION.get(code, 0.0) for code in operation_codes) budget = world_model.get("budget") if isinstance(world_model.get("budget"), dict) else {} budget_amount = float(budget.get("budget_amount_usd") or 0.0) current_ratio = float(budget.get("usage_ratio") or 0.0) projected_budget_ratio = current_ratio + (estimated_budget_delta / budget_amount if budget_amount > 0 else 0.0) capacity = world_model.get("capacity") if isinstance(world_model.get("capacity"), list) else [] current_peak_ratio = max([normalize_workload_ratio(item.get("workload_ratio")) for item in capacity], default=0.0) project_member_count = max(1, int(world_model.get("member_count") or len(capacity) or 1)) projected_capacity_peak_ratio = current_peak_ratio + (estimated_capacity_delta / (40.0 * project_member_count)) task_summary = world_model.get("task_summary") if isinstance(world_model.get("task_summary"), dict) else {} blocked_count = int((task_summary.get("by_status") or {}).get("blocked") or 0) overdue_count = int(task_summary.get("overdue_count") or 0) missing_skill_tag_ids = task_summary.get("uncovered_skill_tag_ids") if isinstance(task_summary.get("uncovered_skill_tag_ids"), list) else [] charter = world_model.get("charter") if isinstance(world_model.get("charter"), list) else [] dependency_pressure = "high" if blocked_count > 0 or overdue_count > 0 else ("medium" if not charter else "low") projected_risk_score = min( 1.0, 0.12 + projected_budget_ratio * 0.24 + projected_capacity_peak_ratio * 0.28 + blocked_count * 0.08 + overdue_count * 0.05 + len(missing_skill_tag_ids) * 0.07, ) return { "intent": intent.name, "operation_codes": operation_codes, "estimated_budget_delta_usd": round(estimated_budget_delta, 2), "projected_budget_usage_ratio": round(projected_budget_ratio, 4), "estimated_capacity_delta_hours": round(estimated_capacity_delta, 2), "projected_capacity_peak_ratio": round(projected_capacity_peak_ratio, 4), "blocked_task_count": blocked_count, "overdue_task_count": overdue_count, "dependency_pressure": dependency_pressure, "missing_skill_tag_ids": missing_skill_tag_ids[:20], "missing_skill_count": len(missing_skill_tag_ids), "projected_risk_score": round(projected_risk_score, 4), }