# ============================================================ # Plan-layer MPS / S&OP (moduleId: domain-mps-planning, R71.4) # Long-term master plan draft + finite rough-cut capacity check # + plan -> schedule -> publish -> execution feedback trace. # Read-only projections and lightweight trace storage only; # existing PlanStore governance is reused, never mutated. # ============================================================ from __future__ import annotations import copy import hashlib import json import os import tempfile import threading import uuid from datetime import UTC, datetime from decimal import Decimal from typing import Any from server.aps_domain.planning import WARN_RATIO, build_bucket_skeleton, daily_capacity from server.timeutil import add_minutes, fmt_date, parse_dt, today0 World = dict[str, Any] MPS_CAPACITY_MODES = {"FINITE", "INFINITE"} MPS_LONG_CYCLE_THRESHOLD_DAYS = 30.0 _DRAFT_HASH_V1 = "mps-draft-hash.v1" _DRAFT_HASH_V1_NUMERIC = "mps-draft-hash.v1-json-numbers" _DRAFT_HASH_V2 = "mps-draft-hash.v2" _DRAFT_BINDING_KEYS = frozenset( {"draftHash", "draftHashVersion", "scope", "worldFingerprint"} ) _MPS_DERIVED_WORLD_KEYS = frozenset( { "scheduleVersions", "productionOrders", "workOrders", "conflicts", "flexScheduleVersions", "flexVirtualLines", "flexWorkOrders", "flexConflicts", } ) _SCHEDULE_LIFECYCLE_KEYS = frozenset( { "status", "publishedAt", "publishReady", "dispatchReady", "completedQuantity", "progressPercent", "actualStartTime", "actualEndTime", "reportedAt", } ) def _now_iso() -> str: return datetime.now(UTC).isoformat().replace("+00:00", "Z") def _canonical_hash(value: Any) -> str: payload = json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False, default=str, ).encode("utf-8") return hashlib.sha256(payload).hexdigest() def _scope_payload( *, tenant_uuid: str, project_id: str, world_key: str, ) -> dict[str, str]: values = { "tenantUuid": str(tenant_uuid or "").strip(), "projectId": str(project_id or "").strip(), "worldKey": str(world_key or "").strip(), } if not all(values.values()): raise ValueError("MPS scope requires tenantUuid, projectId, and worldKey") return values def _draft_inputs(draft: dict[str, Any]) -> dict[str, Any]: return { "mode": str(draft.get("mode") or "WEEK").upper(), "startDate": str(draft.get("startDate") or "")[:10], "horizonDays": int(draft.get("horizonDays") or 0), "includeForecast": bool(draft.get("includeForecast", True)), "includeFirm": bool(draft.get("includeFirm", True)), "longCycleOnly": bool(draft.get("longCycleOnly", False)), "capacityMode": str(draft.get("capacityMode") or "FINITE").upper(), } def _draft_hash(draft: dict[str, Any]) -> str: version = str(draft.get("draftHashVersion") or _DRAFT_HASH_V1) return _draft_hash_for_version(draft, version) def _draft_hash_for_version(draft: dict[str, Any], version: str) -> str: payload = { key: value for key, value in copy.deepcopy(draft).items() if key not in _DRAFT_BINDING_KEYS and key != "planId" } if version == _DRAFT_HASH_V1: return _canonical_hash(payload) if version == _DRAFT_HASH_V1_NUMERIC: return _canonical_hash(_normalize_json_numbers_v1(payload)) if version == _DRAFT_HASH_V2: return _canonical_hash(_typed_json_tree(payload)) raise ValueError(f"unsupported MPS draft hash version: {version}") def _resolve_draft_hash_version(draft: dict[str, Any]) -> str: """Resolve explicit v2 or either unversioned historical hash algorithm.""" supplied_hash = str(draft.get("draftHash") or "") explicit = str(draft.get("draftHashVersion") or "") if explicit: if explicit not in {_DRAFT_HASH_V1, _DRAFT_HASH_V1_NUMERIC, _DRAFT_HASH_V2}: raise ValueError("MPS draft hash version is not supported") if supplied_hash != _draft_hash_for_version(draft, explicit): raise ValueError("MPS draft content hash mismatch") return explicit for legacy in (_DRAFT_HASH_V1, _DRAFT_HASH_V1_NUMERIC): if supplied_hash == _draft_hash_for_version(draft, legacy): return legacy raise ValueError("MPS draft content hash mismatch") def _json_number_text(value: int | float | Decimal) -> str: """Return one decimal representation for the same JSON numeric value.""" number = Decimal(str(value)) if not number.is_finite(): raise ValueError("MPS draft contains a non-finite number") text = format(number.normalize(), "f") if "." in text: text = text.rstrip("0").rstrip(".") return "0" if text in {"", "-0"} else text def _normalize_json_numbers_v1(value: Any) -> Any: """Exact interim unversioned algorithm kept only for trace migration.""" if value is None or isinstance(value, (str, bool)): return value if isinstance(value, (int, float, Decimal)): return {"__apsJsonNumber__": _json_number_text(value)} if isinstance(value, dict): return {str(key): _normalize_json_numbers_v1(item) for key, item in value.items()} if isinstance(value, (list, tuple)): return [_normalize_json_numbers_v1(item) for item in value] return str(value) def _typed_json_tree(value: Any) -> Any: """Encode every JSON type so internal hash nodes cannot collide with business data.""" if value is None: return ["null"] if isinstance(value, bool): return ["bool", value] if isinstance(value, (int, float, Decimal)): return ["number", _json_number_text(value)] if isinstance(value, str): return ["string", value] if isinstance(value, dict): items: dict[str, Any] = {} for key, item in value.items(): string_key = str(key) if string_key in items: raise ValueError(f"MPS draft has duplicate JSON object key: {string_key}") items[string_key] = _typed_json_tree(item) return ["object", [[key, items[key]] for key in sorted(items)]] if isinstance(value, (list, tuple)): return ["array", [_typed_json_tree(item) for item in value]] return ["stringified", type(value).__qualname__, str(value)] def _world_fingerprint(world: World) -> str: from server.agent_core.harness import world_fingerprint planning_inputs = { key: value for key, value in (world or {}).items() if key not in _MPS_DERIVED_WORLD_KEYS } return world_fingerprint(planning_inputs) def bind_mps_draft( world: World, draft: dict[str, Any], *, tenant_uuid: str, project_id: str, world_key: str, ) -> dict[str, Any]: """Bind a server-generated draft to one tenant/project world snapshot.""" bound = copy.deepcopy(draft) bound["scope"] = _scope_payload( tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) bound["worldFingerprint"] = _world_fingerprint(world) bound["draftHashVersion"] = _DRAFT_HASH_V2 bound["draftHash"] = _draft_hash(bound) return bound def validate_mps_draft_binding( world: World, draft: dict[str, Any], *, tenant_uuid: str, project_id: str, world_key: str, allow_legacy: bool = False, ) -> dict[str, Any]: """Fail closed unless a draft is reproducible in the current scoped world.""" if not isinstance(draft, dict) or draft.get("kind") != "MPS_DRAFT": raise ValueError("MPS draft payload is invalid") expected_scope = _scope_payload( tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) if draft.get("scope") != expected_scope: raise ValueError("MPS draft scope does not match the authenticated project") current_world_fingerprint = _world_fingerprint(world) if draft.get("worldFingerprint") != current_world_fingerprint: raise ValueError("MPS draft world fingerprint is stale") supplied_hash = str(draft.get("draftHash") or "") hash_version = _resolve_draft_hash_version(draft) if hash_version != _DRAFT_HASH_V2 and not allow_legacy: raise ValueError("MPS draft persist/gap requires draftHashVersion v2") inputs = _draft_inputs(draft) rebuilt = build_mps_draft( world, mode=inputs["mode"], start_date=inputs["startDate"], horizon_days=inputs["horizonDays"], include_forecast=inputs["includeForecast"], include_firm=inputs["includeFirm"], long_cycle_only=inputs["longCycleOnly"], capacity_mode=inputs["capacityMode"], ) rebuilt["draftId"] = draft.get("draftId") if draft.get("draftHashVersion"): rebuilt["draftHashVersion"] = hash_version if _draft_hash_for_version(rebuilt, hash_version) != supplied_hash: raise ValueError("MPS draft cannot be reproduced from the current world") return copy.deepcopy(draft) def _input_summary( draft: dict[str, Any], *, tenant_uuid: str, project_id: str, world_key: str, ) -> dict[str, Any]: return { **_scope_payload( tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ), "draftId": draft.get("draftId"), "draftHash": draft.get("draftHash"), "draftHashVersion": _resolve_draft_hash_version(draft), "worldFingerprint": draft.get("worldFingerprint"), "parameters": _draft_inputs(draft), } def _as_date(value: str) -> Any: return parse_dt(str(value)[:10] + " 00:00") def _shift_minutes( start_time: str, end_time: str, breaks: list[dict[str, Any]] ) -> float: try: start = parse_dt(f"2000-01-01 {start_time}") end = parse_dt(f"2000-01-01 {end_time}") minutes = (end - start).total_seconds() / 60.0 for item in breaks or []: b0 = parse_dt(f"2000-01-01 {item.get('start')}") b1 = parse_dt(f"2000-01-01 {item.get('end')}") minutes -= (b1 - b0).total_seconds() / 60.0 return max(float(minutes), 60.0) except (TypeError, ValueError): return 480.0 def _calendar_context(world: World) -> dict[str, Any]: flex = world.get("flexCalendar") or [] if flex: cal = flex[0] return { "source": "flexCalendar", "workdays": list(cal.get("workdays") or [1, 2, 3, 4, 5]), "shiftMinutes": _shift_minutes( str(cal.get("startTime") or "08:00"), str(cal.get("endTime") or "17:00"), list(cal.get("breaks") or []), ), } shift_rows = world.get("shiftCalendar") or [] if shift_rows: working_dates = sorted( { str(row["date"]) for row in shift_rows if row.get("isWorking") is not False } ) total = 0.0 for shift in world.get("shifts") or []: if str(shift.get("status") or "ACTIVE").upper() not in ("ACTIVE",): continue total += _shift_minutes( str(shift.get("startTime") or "08:00"), str(shift.get("endTime") or "17:00"), list(shift.get("breakPeriods") or []), ) return { "source": "shiftCalendar", "workingDates": working_dates, "shiftMinutes": max(total, 60.0), } return { "source": "default", "workdays": [1, 2, 3, 4, 5], "shiftMinutes": 480.0, } def _is_workday(cal: dict[str, Any], date_s: str) -> bool: if cal.get("source") == "shiftCalendar": return date_s in set(cal.get("workingDates") or []) try: return parse_dt(date_s + " 00:00").isoweekday() in set( cal.get("workdays") or [1, 2, 3, 4, 5] ) except (TypeError, ValueError): return True def _bucket_workdays(cal: dict[str, Any], bucket: dict[str, Any]) -> int: start = _as_date(bucket["start"]) end = _as_date(bucket["end"]) count = 0 current = start while current <= end: if _is_workday(cal, fmt_date(current)): count += 1 current = add_minutes(current, 24 * 60) return count def _order_lead_time_days(order: dict[str, Any]) -> float: raw = order.get("leadTimeDays") if raw is not None: try: return float(raw) except (TypeError, ValueError): pass values = [item.get("leadTimeDays") for item in order.get("items") or []] parsed = [float(v) for v in values if v is not None] return max(parsed) if parsed else 0.0 def _collect_mps_demands( world: World, *, include_forecast: bool, include_firm: bool, long_cycle_only: bool, ) -> list[dict[str, Any]]: from server.aps_domain.forecast import ensure_forecast_table from server.aps_domain.orders import SCHEDULABLE_STATUSES points: list[dict[str, Any]] = [] if include_firm: for so in world.get("salesOrders") or []: if so.get("status") not in SCHEDULABLE_STATUSES: continue due = str(so.get("deliveryDate") or "")[:10] if not due: continue is_long = ( bool(so.get("isLongCycle")) or _order_lead_time_days(so) >= MPS_LONG_CYCLE_THRESHOLD_DAYS ) if long_cycle_only and not is_long: continue for item in so.get("items") or []: if item.get("status") in ("CANCELLED", "COMPLETED"): continue qty = int(item.get("quantity") or 0) if qty <= 0: continue points.append( { "source": "LONG_CYCLE" if is_long else "FIRM", "ref": so.get("orderNo"), "productId": item.get("productId"), "productCode": item.get("productCode"), "productName": item.get("productName"), "quantity": qty, "weightedQty": float(qty), "dueDate": due, "leadTimeDays": _order_lead_time_days(so), } ) if include_forecast: ensure_forecast_table(world) for fc in world.get("forecastOrders") or []: if fc.get("status") not in ("ACTIVE",): continue due = str(fc.get("dueDate") or fc.get("periodEnd") or "")[:10] if not due: continue qty = int(fc.get("quantity") or 0) if qty <= 0: continue confidence = float( fc.get("confidence") if fc.get("confidence") is not None else 1.0 ) confidence = max(0.0, min(1.0, confidence)) points.append( { "source": "FORECAST", "ref": fc.get("forecastNo"), "productId": fc.get("productId"), "productCode": fc.get("productCode"), "productName": fc.get("productName"), "quantity": qty, "weightedQty": round(qty * confidence, 1), "dueDate": due, "confidence": confidence, "leadTimeDays": 0, } ) return points def _bucket_for_date( buckets: list[dict[str, Any]], date_s: str ) -> dict[str, Any] | None: if not buckets: return None if date_s < buckets[0]["start"]: return buckets[0] for bucket in buckets: if bucket["start"] <= date_s <= bucket["end"]: return bucket return None def _status_for(ratio: float) -> str: if ratio >= 1.0: return "OVER" if ratio >= WARN_RATIO: return "WARN" return "OK" def _commitment(status: str) -> str: if status == "OVER": return "INFEASIBLE" if status == "WARN": return "AT_RISK" return "FEASIBLE" def assess_mps_capacity(world: World, draft: dict[str, Any]) -> dict[str, Any]: """Rough-cut finite capacity assessment for an MPS draft.""" buckets = draft.get("buckets") or [] total_weighted = sum(float(b.get("weightedDemand") or 0) for b in buckets) feasible_weighted = sum( float(b.get("weightedDemand") or 0) for b in buckets if b.get("status") != "OVER" ) over = sum(1 for b in buckets if b.get("status") == "OVER") warn = sum(1 for b in buckets if b.get("status") == "WARN") bottleneck_buckets = sorted( [b for b in buckets if b.get("status") == "OVER"], key=lambda b: float(b.get("loadRatio") or 0), reverse=True, ) bottlenecks: list[dict[str, Any]] = [] for bucket in bottleneck_buckets[:5]: drivers = [ { "productCode": item.get("productCode"), "productName": item.get("productName"), "weightedQty": item.get("weightedQty"), } for item in (bucket.get("byProduct") or [])[:3] ] workdays = int(bucket.get("workdays") or 1) bottlenecks.append( { "key": bucket.get("key"), "label": bucket.get("label"), "loadRatio": bucket.get("loadRatio"), "gap": bucket.get("gap"), "requiredDaily": round( float(bucket.get("weightedDemand") or 0) / workdays, 1 ), "drivers": drivers, } ) peak_load = max((float(b.get("loadRatio") or 0) for b in buckets), default=0.0) feasible_rate = ( round(feasible_weighted / total_weighted, 3) if total_weighted else 1.0 ) feasible_bucket_rate = ( round((len(buckets) - over) / len(buckets), 3) if buckets else 1.0 ) if over: first = bottlenecks[0] hint = ( f"Finite capacity OVER in {over} bucket(s); earliest bottleneck {first.get('label')} " f"at load {first.get('loadRatio')}. Review leveling/outsourcing before trial scheduling." ) elif warn: hint = ( f"No overloaded bucket, but {warn} bucket(s) at or above {WARN_RATIO:.0%} load. " "Monitor the warning window before commit." ) else: hint = "Finite rough-cut capacity is feasible across the MPS horizon." return { "capacityMode": draft.get("capacityMode") or "FINITE", "dailyCapacity": draft.get("dailyCapacity") or daily_capacity(world), "feasibleRate": feasible_rate, "feasibleBucketRate": feasible_bucket_rate, "overCount": over, "warnCount": warn, "okCount": len(buckets) - over - warn, "peakLoadRatio": round(peak_load, 3), "bottleneckBucketCount": len(bottleneck_buckets), "bottlenecks": bottlenecks, "hint": hint, } def build_mps_draft( world: World, *, mode: str = "WEEK", start_date: str | None = None, horizon_days: int = 90, include_forecast: bool = True, include_firm: bool = True, long_cycle_only: bool = False, capacity_mode: str = "FINITE", ) -> dict[str, Any]: """Build a bucketed long-term MPS draft from forecasts and firm/long-cycle orders.""" cap_mode = (capacity_mode or "FINITE").upper() if cap_mode not in MPS_CAPACITY_MODES: raise ValueError("capacityMode must be FINITE or INFINITE") skeleton = build_bucket_skeleton( mode=mode, start_date=start_date, horizon_days=horizon_days ) calendar = _calendar_context(world) shift_minutes = float(calendar.get("shiftMinutes") or 480.0) day_cap = daily_capacity(world) points = _collect_mps_demands( world, include_forecast=include_forecast, include_firm=include_firm, long_cycle_only=long_cycle_only, ) buckets: list[dict[str, Any]] = [] for raw in skeleton: workdays = _bucket_workdays(calendar, raw) capacity = round(day_cap * workdays, 1) if cap_mode == "FINITE" else None buckets.append( { **raw, "workdays": workdays, "workHours": round(workdays * shift_minutes / 60.0, 1), "capacityMode": cap_mode, "capacity": capacity, "firmQty": 0, "forecastQty": 0, "demandQty": 0, "weightedDemand": 0.0, "loadRatio": 0.0, "gap": 0.0, "status": "OK", "byProduct": {}, "mpsLines": [], } ) unbucketed: list[dict[str, Any]] = [] line_no = 0 for point in points: bucket = _bucket_for_date(buckets, point["dueDate"]) if bucket is None: unbucketed.append( { **point, "planLineId": f"ML-UNBUCKETED-{len(unbucketed) + 1:03d}", "reason": "OUT_OF_HORIZON", } ) continue line_no += 1 line = { "planLineId": f"ML-{bucket['key']}-{line_no:03d}", "source": point["source"], "ref": point["ref"], "productId": point["productId"], "productCode": point["productCode"], "productName": point["productName"], "quantity": point["quantity"], "weightedQty": point["weightedQty"], "dueDate": point["dueDate"], "bucketKey": bucket["key"], "recommendedStartDate": bucket["start"], "plannedOrderQty": point["quantity"], "leadTimeDays": round(float(point.get("leadTimeDays") or 0)), } bucket["mpsLines"].append(line) bucket["demandQty"] += point["quantity"] bucket["weightedDemand"] += point["weightedQty"] if point["source"] == "FORECAST": bucket["forecastQty"] += point["quantity"] else: bucket["firmQty"] += point["quantity"] product = bucket["byProduct"].setdefault( point["productCode"], { "productCode": point["productCode"], "productName": point["productName"], "firmQty": 0, "forecastQty": 0, "weightedQty": 0.0, "lineCount": 0, }, ) product["weightedQty"] = round(product["weightedQty"] + point["weightedQty"], 1) product["lineCount"] += 1 if point["source"] == "FORECAST": product["forecastQty"] += point["quantity"] else: product["firmQty"] += point["quantity"] for bucket in buckets: if cap_mode == "FINITE" and bucket["capacity"] and bucket["capacity"] > 0: ratio = round( float(bucket["weightedDemand"]) / float(bucket["capacity"]), 3 ) gap = round(float(bucket["weightedDemand"]) - float(bucket["capacity"]), 1) else: ratio, gap = 0.0, 0.0 status = _status_for(ratio) if cap_mode == "FINITE" else "OK" bucket["loadRatio"] = ratio bucket["gap"] = gap bucket["status"] = status bucket["byProduct"] = sorted( bucket["byProduct"].values(), key=lambda item: -(float(item["firmQty"]) + float(item["forecastQty"])), ) for line in bucket["mpsLines"]: line["commitment"] = _commitment(status) line["commitmentReason"] = f"bucket status {status}" total_firm = sum(int(b["firmQty"]) for b in buckets) total_forecast = sum(int(b["forecastQty"]) for b in buckets) total_weighted = sum(float(b["weightedDemand"]) for b in buckets) total_capacity = sum(float(b["capacity"] or 0) for b in buckets) over_count = sum(1 for b in buckets if b["status"] == "OVER") warn_count = sum(1 for b in buckets if b["status"] == "WARN") line_count = sum(len(b["mpsLines"]) for b in buckets) feasible_lines = sum( 1 for b in buckets for line in b["mpsLines"] if line["commitment"] != "INFEASIBLE" ) draft = { "kind": "MPS_DRAFT", "draftId": f"MPS-{fmt_date(today0()).replace('-', '')}-{uuid.uuid4().hex[:8].upper()}", "planId": None, "mode": (mode or "WEEK").upper(), "capacityMode": cap_mode, "startDate": skeleton[0]["start"] if skeleton else fmt_date(today0()), "endDate": skeleton[-1]["end"] if skeleton else fmt_date(today0()), "horizonDays": int(horizon_days), "includeForecast": include_forecast, "includeFirm": include_firm, "longCycleOnly": long_cycle_only, "dailyCapacity": day_cap, "warnThreshold": WARN_RATIO, "calendar": { "source": calendar["source"], "shiftMinutes": round(shift_minutes), "workdays": calendar.get("workdays") or len(calendar.get("workingDates") or []), }, "summary": { "bucketCount": len(buckets), "overCount": over_count, "warnCount": warn_count, "okCount": len(buckets) - over_count - warn_count, "firmQty": total_firm, "forecastQty": total_forecast, "demandQty": total_firm + total_forecast, "weightedDemand": round(total_weighted, 1), "capacity": round(total_capacity, 1) if cap_mode == "FINITE" else None, "overallLoad": round(total_weighted / total_capacity, 3) if total_capacity else 0.0, "lineCount": line_count, "feasibleLineCount": feasible_lines, "infeasibleLineCount": line_count - feasible_lines, "unbucketedCount": len(unbucketed), }, "buckets": buckets, "unbucketed": unbucketed, } draft["capacity"] = assess_mps_capacity(world, draft) if cap_mode == "INFINITE": draft["hint"] = ( "INFINITE rough-cut capacity: demand is not capped by line capacity; " "finite reference is available in assess_mps_capacity." ) else: draft["hint"] = ( "MPS draft uses the shift calendar for workdays and ACTIVE line daily capacity; " "OVER buckets need leveling/outsourcing before trial scheduling." ) return draft class MpsTraceStore: """Append-only lightweight envelope journal for MPS -> schedule -> publish -> feedback.""" schema_version = "2.0" def __init__( self, path: str, *, tenant_uuid: str, project_id: str, world_key: str, ) -> None: self.path = path self.scope = _scope_payload( tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) self._lock = threading.RLock() self._traces = self._load() def _load(self) -> dict[str, dict[str, Any]]: try: with open(self.path, "r", encoding="utf-8") as handle: document = json.load(handle) except FileNotFoundError: return {} except (json.JSONDecodeError, OSError) as exc: raise ValueError(f"cannot read MPS trace store: {exc}") from exc if not isinstance(document, dict): raise TypeError("MPS trace store document must be a JSON object") traces = document.get("traces") if traces is None: return {} if not isinstance(traces, dict): raise TypeError("MPS trace store traces must be a JSON object") return copy.deepcopy(traces) def _write(self) -> None: directory = os.path.dirname(self.path) or "." os.makedirs(directory, exist_ok=True) fd, temporary = tempfile.mkstemp(dir=directory, suffix=".tmp") try: with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump( {"schemaVersion": self.schema_version, "traces": self._traces}, handle, ensure_ascii=False, ) os.replace(temporary, self.path) except BaseException: if os.path.exists(temporary): os.unlink(temporary) raise def create( self, *, plan_id: str, plan_version: int | None = None, draft_id: str | None = None, draft_snapshot: dict[str, Any], input_summary: dict[str, Any], world_fingerprint: str, trace_id: str | None = None, created_by: str = "SYSTEM", ) -> dict[str, Any]: with self._lock: if any( input_summary.get(key) != value for key, value in self.scope.items() ): raise ValueError( "MPS trace input summary scope does not match its store" ) if draft_snapshot.get("scope") != self.scope: raise ValueError("MPS trace draft scope does not match its store") if draft_snapshot.get("draftHash") != input_summary.get("draftHash"): raise ValueError( "MPS trace draft hash does not match its input summary" ) draft_hash_version = _resolve_draft_hash_version(draft_snapshot) summary_hash_version = str( input_summary.get("draftHashVersion") or draft_hash_version ) if draft_hash_version != summary_hash_version: raise ValueError( "MPS trace draft hash version does not match its input summary" ) if world_fingerprint != input_summary.get("worldFingerprint"): raise ValueError( "MPS trace world fingerprint does not match its input summary" ) resolved_id = trace_id or f"TR-{uuid.uuid4().hex[:12].upper()}" if resolved_id in self._traces: raise ValueError(f"MPS trace {resolved_id!r} already exists") now = _now_iso() envelope: dict[str, Any] = { "traceId": resolved_id, "planId": plan_id, "planVersion": plan_version, "draftId": draft_id, **self.scope, "draftHash": input_summary.get("draftHash"), "draftHashVersion": summary_hash_version, "worldFingerprint": world_fingerprint, "inputSummary": copy.deepcopy(input_summary), "draftSnapshot": copy.deepcopy(draft_snapshot), "scheduleId": None, "scheduleVersionNo": None, "scheduleTrack": None, "scheduleFingerprint": None, "scheduleStatusAtLink": None, "scheduleLinkedAt": None, "scheduleLinkedBy": None, "adoptId": None, "adoptedAt": None, "adoptedBy": None, "publishId": None, "publishedAt": None, "publishedBy": None, "executionFeedback": [], "events": [{"at": now, "event": "OPEN", "by": created_by}], "createdAt": now, "updatedAt": now, } self._traces[resolved_id] = copy.deepcopy(envelope) try: self._write() except BaseException: self._traces.pop(resolved_id, None) raise return copy.deepcopy(envelope) def discard_uncommitted(self, trace_id: str) -> None: """Compensate a freshly opened trace before its Plan node commits.""" with self._lock: envelope = self._require(trace_id) events = envelope.get("events") or [] if ( len(events) != 1 or events[0].get("event") != "OPEN" or envelope.get("scheduleId") is not None or envelope.get("publishId") is not None or envelope.get("executionFeedback") ): raise ValueError("only an uncommitted OPEN MPS trace can be discarded") previous = copy.deepcopy(envelope) del self._traces[trace_id] try: self._write() except BaseException: self._traces[trace_id] = previous raise def _require(self, trace_id: str) -> dict[str, Any]: envelope = self._traces.get(trace_id) if envelope is None or any( envelope.get(key) != value for key, value in self.scope.items() ): raise ValueError(f"MPS trace {trace_id!r} does not exist") return envelope def get(self, trace_id: str) -> dict[str, Any]: with self._lock: return copy.deepcopy(self._require(trace_id)) def list(self) -> list[dict[str, Any]]: with self._lock: rows = [ copy.deepcopy(value) for value in self._traces.values() if all( value.get(key) == expected for key, expected in self.scope.items() ) ] rows.sort(key=lambda row: row.get("createdAt") or "") return rows def replay(self, trace_id: str, *, world: World) -> dict[str, Any]: envelope = self.get(trace_id) draft = validate_mps_draft_binding( world, envelope.get("draftSnapshot") or {}, tenant_uuid=self.scope["tenantUuid"], project_id=self.scope["projectId"], world_key=self.scope["worldKey"], allow_legacy=True, ) summary = envelope.get("inputSummary") or {} if summary.get("draftHash") != draft.get("draftHash"): raise ValueError( "MPS trace input summary does not match its draft snapshot" ) return { "traceId": envelope["traceId"], "planId": envelope["planId"], "planVersion": envelope.get("planVersion"), "inputSummary": copy.deepcopy(summary), "draft": draft, } def link_schedule( self, trace_id: str, schedule_id: Any, *, schedule_version_no: str | None = None, track: str = "fixed", schedule_fingerprint: str | None = None, schedule_status: str | None = None, world_fingerprint: str | None = None, actor: str = "SYSTEM", _persist: bool = True, ) -> dict[str, Any]: with self._lock: envelope = self._require(trace_id) normalized_track = str(track or "").lower() if normalized_track not in {"fixed", "flex"}: raise ValueError("MPS schedule track must be fixed or flex") if schedule_id is None: raise ValueError("MPS schedule id is required") if ( world_fingerprint is not None and envelope.get("worldFingerprint") != world_fingerprint ): raise ValueError("MPS schedule world fingerprint does not match its trace") existing_id = envelope.get("scheduleId") if existing_id is not None: previous = copy.deepcopy(envelope) expected = { "scheduleId": schedule_id, "scheduleVersionNo": schedule_version_no, "scheduleTrack": normalized_track, "scheduleFingerprint": schedule_fingerprint, } mismatched = [ key for key, value in expected.items() if value is not None and envelope.get(key) is not None and ( not _same_id(envelope.get(key), value) if key == "scheduleId" else envelope.get(key) != value ) ] if mismatched: raise ValueError( "MPS trace already links a different schedule version" ) backfilled = False for key, value in expected.items(): if envelope.get(key) is None and value is not None: envelope[key] = value backfilled = True if schedule_status and envelope.get("scheduleStatusAtLink") is None: envelope["scheduleStatusAtLink"] = schedule_status backfilled = True if not backfilled: return copy.deepcopy(envelope) now = _now_iso() envelope["updatedAt"] = now try: if _persist: self._write() except BaseException: self._traces[trace_id] = previous raise return copy.deepcopy(envelope) previous = copy.deepcopy(envelope) now = _now_iso() envelope["scheduleId"] = schedule_id envelope["scheduleVersionNo"] = schedule_version_no envelope["scheduleTrack"] = normalized_track envelope["scheduleFingerprint"] = schedule_fingerprint envelope["scheduleStatusAtLink"] = schedule_status envelope["scheduleLinkedAt"] = now envelope["scheduleLinkedBy"] = actor envelope["updatedAt"] = now envelope["events"].append( { "at": now, "event": "SCHEDULE", "by": actor, "detail": { "scheduleId": schedule_id, "scheduleVersionNo": schedule_version_no, "track": envelope["scheduleTrack"], "scheduleFingerprint": schedule_fingerprint, }, } ) try: if _persist: self._write() except BaseException: self._traces[trace_id] = previous raise return copy.deepcopy(envelope) def link_publish( self, trace_id: str, publish_id: Any, *, published_at: str | None = None, fact_type: str = "PUBLISH", schedule_id: Any = None, schedule_version_no: str | None = None, track: str | None = None, schedule_fingerprint: str | None = None, actor: str = "SYSTEM", _persist: bool = True, ) -> dict[str, Any]: with self._lock: envelope = self._require(trace_id) normalized_fact = str(fact_type or "PUBLISH").upper() if normalized_fact not in {"ADOPT", "PUBLISH"}: raise ValueError("MPS local fact must be ADOPT or PUBLISH") if publish_id in (None, ""): raise ValueError("MPS local fact id is required") expected_track = str(track).lower() if track is not None else None expected = { "scheduleId": schedule_id, "scheduleVersionNo": schedule_version_no, "scheduleTrack": expected_track, "scheduleFingerprint": schedule_fingerprint, } for key, value in expected.items(): if value is None: continue current = envelope.get(key) matches = ( _same_id(current, value) if key == "scheduleId" else current == value ) if current is None or not matches: raise ValueError( "MPS local fact does not match the linked schedule version" ) id_key = "adoptId" if normalized_fact == "ADOPT" else "publishId" at_key = "adoptedAt" if normalized_fact == "ADOPT" else "publishedAt" by_key = "adoptedBy" if normalized_fact == "ADOPT" else "publishedBy" existing_fact_id = envelope.get(id_key) if existing_fact_id is not None: if str(existing_fact_id) != str(publish_id): raise ValueError( f"MPS trace already records a different {normalized_fact} fact" ) return copy.deepcopy(envelope) previous = copy.deepcopy(envelope) now = _now_iso() envelope[id_key] = publish_id envelope[at_key] = published_at or now envelope[by_key] = actor envelope["updatedAt"] = now envelope["events"].append( { "at": now, "event": normalized_fact, "by": actor, "detail": { "factId": publish_id, "factType": normalized_fact, "scheduleId": envelope.get("scheduleId"), "scheduleVersionNo": envelope.get("scheduleVersionNo"), "track": envelope.get("scheduleTrack"), "scheduleFingerprint": envelope.get("scheduleFingerprint"), }, } ) try: if _persist: self._write() except BaseException: self._traces[trace_id] = previous raise return copy.deepcopy(envelope) def link_local_fact( self, trace_id: str, *, schedule_id: Any, schedule_version_no: str, track: str, schedule_fingerprint: str, schedule_status: str, world_fingerprint: str, fact_type: str, fact_id: str, fact_at: str | None = None, actor: str = "SYSTEM", ) -> dict[str, Any]: """Atomically bind one local version and its adoption/publication fact.""" with self._lock: previous = copy.deepcopy(self._require(trace_id)) try: self.link_schedule( trace_id, schedule_id, schedule_version_no=schedule_version_no, track=track, schedule_fingerprint=schedule_fingerprint, schedule_status=schedule_status, world_fingerprint=world_fingerprint, actor=actor, _persist=False, ) self.link_publish( trace_id, fact_id, published_at=fact_at, fact_type=fact_type, schedule_id=schedule_id, schedule_version_no=schedule_version_no, track=track, schedule_fingerprint=schedule_fingerprint, actor=actor, _persist=False, ) envelope = self._require(trace_id) idempotent = envelope == previous if not idempotent: self._write() return { "trace": copy.deepcopy(envelope), "idempotent": idempotent, } except BaseException: self._traces[trace_id] = previous raise def link_feedback( self, trace_id: str, *, feedback_id: str, work_order_id: Any, progress_pct: int, status: str, qty_done: Any = None, reported_at: str | None = None, actor: str = "SYSTEM", ) -> dict[str, Any]: with self._lock: envelope = self._require(trace_id) now = _now_iso() feedback = { "feedbackId": feedback_id, "workOrderId": work_order_id, "progressPct": max(0, min(100, int(progress_pct))), "status": status, "qtyDone": qty_done, "reportedAt": reported_at or now, "by": actor, } envelope["executionFeedback"].append(feedback) envelope["updatedAt"] = now envelope["events"].append( { "at": now, "event": "FEEDBACK", "by": actor, "detail": {"feedbackId": feedback_id, "workOrderId": work_order_id}, } ) self._write() return copy.deepcopy(envelope) def create_mps_plan_node( draft: dict[str, Any], *, plan_store: Any, input_summary: dict[str, Any], plan_id: str | None = None, created_by: str = "SYSTEM", ) -> Any: """Persist an MPS draft as an immutable L2 Plan node under a reusable L0/L1 chain.""" from server.agent_core.plan_runtime import PlanNotFoundError l0_id = "intent-mps" l1_id = "strategy-mps" try: plan_store.latest(l0_id) except PlanNotFoundError: plan_store.create( plan_id=l0_id, layer="L0", parent_id=None, inputs={"intent": "mps"}, payload={"kind": "intent", "action": "mps.plan"}, status="APPROVED", created_by="SYSTEM", ) try: plan_store.latest(l1_id) except PlanNotFoundError: plan_store.create( plan_id=l1_id, layer="L1", parent_id=l0_id, inputs={"intent": l0_id}, payload={"kind": "strategy", "action": "mps.plan"}, status="APPROVED", created_by="SYSTEM", ) summary = draft.get("summary") or {} capacity = draft.get("capacity") or {} node = plan_store.create( plan_id=plan_id or f"mps-{uuid.uuid4().hex[:12]}", layer="L2", parent_id=l1_id, inputs=copy.deepcopy(input_summary), payload={ "kind": "mps-draft", "draftId": draft.get("draftId"), "draftHash": draft.get("draftHash"), "worldFingerprint": draft.get("worldFingerprint"), "inputSummary": copy.deepcopy(input_summary), "draftSnapshot": copy.deepcopy(draft), "summary": { "bucketCount": summary.get("bucketCount"), "weightedDemand": summary.get("weightedDemand"), "overCount": summary.get("overCount"), "warnCount": summary.get("warnCount"), "overallLoad": summary.get("overallLoad"), "feasibleLineCount": summary.get("feasibleLineCount"), }, "capacity": { "feasibleRate": capacity.get("feasibleRate"), "peakLoadRatio": capacity.get("peakLoadRatio"), "bottleneckBucketCount": capacity.get("bottleneckBucketCount"), }, "bucketSummary": [ { "key": b.get("key"), "label": b.get("label"), "status": b.get("status"), "loadRatio": b.get("loadRatio"), "gap": b.get("gap"), } for b in (draft.get("buckets") or []) ], }, status="DRAFT", evidence_refs=[f"mps-draft:{draft.get('draftId')}"], created_by=created_by, ) return node def create_mps_trace( plan_node: Any, *, trace_store: MpsTraceStore, draft: dict[str, Any], input_summary: dict[str, Any], trace_id: str | None = None, created_by: str = "SYSTEM", ) -> dict[str, Any]: return trace_store.create( plan_id=plan_node.planId, plan_version=plan_node.version, draft_id=draft.get("draftId"), draft_snapshot=draft, input_summary=input_summary, world_fingerprint=str(draft.get("worldFingerprint") or ""), trace_id=trace_id, created_by=created_by, ) def persist_mps_draft( draft: dict[str, Any], *, world: World, plan_store: Any, trace_store: MpsTraceStore, tenant_uuid: str, project_id: str, world_key: str, plan_id: str | None = None, created_by: str = "SYSTEM", actor: str = "SYSTEM", ) -> dict[str, Any]: expected_scope = _scope_payload( tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) if trace_store.scope != expected_scope: raise ValueError("MPS trace store scope does not match the draft scope") validated = validate_mps_draft_binding( world, draft, tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) input_summary = _input_summary( validated, tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) resolved_plan_id = plan_id or f"mps-{uuid.uuid4().hex[:12]}" persisted_draft = copy.deepcopy(validated) persisted_draft["planId"] = resolved_plan_id envelope = trace_store.create( plan_id=resolved_plan_id, plan_version=1, draft_id=persisted_draft.get("draftId"), draft_snapshot=persisted_draft, input_summary=input_summary, world_fingerprint=str(persisted_draft.get("worldFingerprint") or ""), created_by=actor, ) try: node = create_mps_plan_node( validated, plan_store=plan_store, input_summary=input_summary, plan_id=resolved_plan_id, created_by=created_by, ) except BaseException as exc: try: trace_store.discard_uncommitted(envelope["traceId"]) except BaseException as rollback_exc: raise RuntimeError( f"MPS Plan persistence failed and trace compensation failed: {rollback_exc}" ) from exc raise return {"planNode": node, "envelope": envelope, "draft": persisted_draft} def get_mps_trace(trace_id: str, *, trace_store: MpsTraceStore) -> dict[str, Any]: return trace_store.get(trace_id) def replay_mps_trace( trace_id: str, *, world: World, trace_store: MpsTraceStore, ) -> dict[str, Any]: return trace_store.replay(trace_id, world=world) def _resolve_schedule_version( world: World, track: str, schedule_id: Any = None, ) -> dict[str, Any] | None: key = "flexScheduleVersions" if track == "flex" else "scheduleVersions" versions = world.get(key) or [] if schedule_id is None: return versions[-1] if versions else None for version in versions: try: if int(version.get("id")) == int(schedule_id): return version except (TypeError, ValueError): if str(version.get("id")) == str(schedule_id): return version return None def _same_id(left: Any, right: Any) -> bool: try: return int(left) == int(right) except (TypeError, ValueError): return str(left) == str(right) def _without_schedule_lifecycle(row: dict[str, Any]) -> dict[str, Any]: return { key: copy.deepcopy(value) for key, value in row.items() if key not in _SCHEDULE_LIFECYCLE_KEYS } def _schedule_projection( world: World, track: str, version: dict[str, Any], ) -> dict[str, Any]: normalized_track = str(track or "").lower() if normalized_track not in {"fixed", "flex"}: raise ValueError("MPS schedule track must be fixed or flex") version_id = version.get("id") if version_id is None: raise ValueError("MPS schedule version is missing its id") if normalized_track == "flex": artifact_specs = ( ("virtualLines", "flexVirtualLines", "versionId"), ("workOrders", "flexWorkOrders", "versionId"), ("conflicts", "flexConflicts", "versionId"), ) production_orders: list[dict[str, Any]] = [] else: production_orders = [ row for row in (world.get("productionOrders") or []) if _same_id(row.get("schedulingVersionId"), version_id) ] production_order_ids = {str(row.get("id")) for row in production_orders} artifact_specs = ( ("workOrders", "workOrders", "productionOrderId"), ("conflicts", "conflicts", "versionId"), ) projection: dict[str, Any] = { "track": normalized_track, "version": _without_schedule_lifecycle(version), } if normalized_track == "fixed": projection["productionOrders"] = sorted( (_without_schedule_lifecycle(row) for row in production_orders), key=lambda row: str(row.get("id")), ) for output_key, world_key, foreign_key in artifact_specs: rows = world.get(world_key) or [] if normalized_track == "fixed" and world_key == "workOrders": selected = [ row for row in rows if str(row.get(foreign_key)) in production_order_ids ] else: selected = [ row for row in rows if _same_id(row.get(foreign_key), version_id) ] projection[output_key] = sorted( (_without_schedule_lifecycle(row) for row in selected), key=lambda row: str(row.get("id")), ) return projection def schedule_fingerprint( world: World, track: str, version: dict[str, Any], ) -> str: """Stable hash of one exact local schedule and its version-owned artifacts.""" return _canonical_hash(_schedule_projection(world, track, version)) def _schedule_summary( world: World, track: str, version: dict[str, Any], ) -> dict[str, Any]: return { "track": track, "scheduleId": version.get("id"), "scheduleVersionNo": version.get("versionNo"), "status": version.get("status"), "createdAt": version.get("createdAt"), "publishedAt": version.get("publishedAt"), "strategy": version.get("sortMode") or version.get("strategy") or version.get("note"), "engineType": version.get("engineType"), "orderCount": int(version.get("orderCount") or 0), "woCount": int(version.get("woCount") or 0), "conflictCount": int(version.get("conflictCount") or 0), "scheduleFingerprint": schedule_fingerprint(world, track, version), } def list_mps_schedule_versions(world: World) -> list[dict[str, Any]]: rows = [ _schedule_summary(world, track, version) for track, key in ( ("fixed", "scheduleVersions"), ("flex", "flexScheduleVersions"), ) for version in (world.get(key) or []) ] rows.sort( key=lambda row: ( str(row.get("createdAt") or ""), str(row.get("scheduleVersionNo") or ""), str(row.get("track") or ""), ), reverse=True, ) return rows def _require_schedule_version( world: World, *, track: str, schedule_id: Any, schedule_version_no: str, expected_fingerprint: str, ) -> tuple[dict[str, Any], dict[str, Any]]: normalized_track = str(track or "").lower() if normalized_track not in {"fixed", "flex"}: raise ValueError("MPS schedule track must be fixed or flex") version = _resolve_schedule_version(world, normalized_track, schedule_id) if version is None: raise ValueError("MPS schedule version does not exist on the requested track") actual_version_no = str(version.get("versionNo") or "") if not schedule_version_no or actual_version_no != str(schedule_version_no): raise ValueError("MPS schedule version number does not match its id and track") summary = _schedule_summary(world, normalized_track, version) if summary["scheduleFingerprint"] != str(expected_fingerprint or ""): raise ValueError("MPS schedule fingerprint is stale") return version, summary def link_mps_trace_local_fact( world: World, *, trace_store: MpsTraceStore, trace_id: str, tenant_uuid: str, project_id: str, world_key: str, draft_hash: str, world_fingerprint: str, schedule_id: Any, schedule_version_no: str, schedule_fingerprint: str, track: str, fact_type: str, fact_id: str | None = None, actor: str = "SYSTEM", ) -> dict[str, Any]: """Bind an exact local schedule and ADOPT/PUBLISH fact without world writes.""" expected_scope = _scope_payload( tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) if trace_store.scope != expected_scope: raise ValueError("MPS trace store scope does not match the local link scope") trace = trace_store.get(trace_id) if any(trace.get(key) != value for key, value in expected_scope.items()): raise ValueError("MPS trace scope does not match the authenticated project") if trace.get("draftHash") != str(draft_hash or ""): raise ValueError("MPS local link draft hash does not match its trace") if trace.get("worldFingerprint") != str(world_fingerprint or ""): raise ValueError("MPS local link world fingerprint does not match its trace") validate_mps_draft_binding( world, trace.get("draftSnapshot") or {}, tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, allow_legacy=True, ) normalized_track = str(track or "").lower() version, schedule = _require_schedule_version( world, track=normalized_track, schedule_id=schedule_id, schedule_version_no=schedule_version_no, expected_fingerprint=schedule_fingerprint, ) normalized_fact = str(fact_type or "").upper() if normalized_fact not in {"ADOPT", "PUBLISH"}: raise ValueError("MPS local fact must be ADOPT or PUBLISH") schedule_status = str(version.get("status") or "").upper() if schedule_status not in {"DRAFT", "PUBLISHED", "DISPATCHED"}: raise ValueError("MPS local fact requires a usable local schedule version") fact_at = None if normalized_fact == "PUBLISH": if schedule_status not in {"PUBLISHED", "DISPATCHED"}: raise ValueError("MPS PUBLISH fact requires a published local version") fact_at = str(version.get("publishedAt") or "") if not fact_at: raise ValueError("MPS PUBLISH fact requires the version publishedAt value") resolved_fact_id = str( fact_id or f"{normalized_fact}:{normalized_track}:{schedule_version_no}" ) linked = trace_store.link_local_fact( trace_id, schedule_id=schedule_id, schedule_version_no=schedule_version_no, track=normalized_track, schedule_fingerprint=schedule_fingerprint, schedule_status=schedule_status, world_fingerprint=world_fingerprint, fact_type=normalized_fact, fact_id=resolved_fact_id, fact_at=fact_at, actor=actor, ) envelope = linked["trace"] at_key = "adoptedAt" if normalized_fact == "ADOPT" else "publishedAt" return { **linked, "schedule": schedule, "fact": { "factId": resolved_fact_id, "factType": normalized_fact, "at": envelope.get(at_key), "by": actor, }, } def _schedule_kpi(world: World, track: str, version: dict[str, Any]) -> dict[str, Any]: order_count = int(version.get("orderCount") or 0) on_time = int(version.get("onTimeCount") or 0) return { "versionNo": version.get("versionNo"), "status": version.get("status"), "orderCount": order_count, "poCount": int(version.get("poCount") or 0), "woCount": int(version.get("woCount") or 0), "conflictCount": int(version.get("conflictCount") or 0), "totalTardiness": round(float(version.get("totalTardiness") or 0), 1), "avgUtilization": round(float(version.get("avgUtilization") or 0), 3), "onTimeCount": on_time, "onTimeRate": round(on_time / order_count, 3) if order_count else None, "makespan": version.get("makespan"), "bottleneck": version.get("bottleneck"), "track": track, } def _planned_quantity(world: World, track: str, version: dict[str, Any]) -> float: version_id = version.get("id") if track == "flex": rows = world.get("flexVirtualLines") or [] return float( sum( int(row.get("quantity") or 0) for row in rows if row.get("versionId") == version_id ) ) rows = world.get("productionOrders") or [] return float( sum( int(row.get("quantity") or 0) for row in rows if row.get("schedulingVersionId") == version_id ) ) def compare_mps_schedule_gap( world: World, draft: dict[str, Any], *, tenant_uuid: str, project_id: str, world_key: str, schedule_id: Any = None, track: str = "fixed", trace: dict[str, Any] | None = None, ) -> dict[str, Any]: """Compare plan-layer MPS KPI with detailed scheduling KPI and summarize gaps.""" expected_scope = _scope_payload( tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, ) trusted_trace_draft = (trace or {}).get("draftSnapshot") or None allow_legacy_effective = bool( trace and isinstance(trusted_trace_draft, dict) and draft == trusted_trace_draft ) effective_draft = validate_mps_draft_binding( world, draft, tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, allow_legacy=allow_legacy_effective, ) tr = str(track or "fixed").lower() if trace: if any(trace.get(key) != value for key, value in expected_scope.items()): raise ValueError("MPS trace scope does not match the authenticated project") if trace.get("worldFingerprint") != effective_draft.get("worldFingerprint"): raise ValueError("MPS trace is bound to a different world version") trace_draft = validate_mps_draft_binding( world, trace.get("draftSnapshot") or {}, tenant_uuid=tenant_uuid, project_id=project_id, world_key=world_key, allow_legacy=True, ) if trace.get("draftHash") != effective_draft.get("draftHash"): raise ValueError("MPS draft does not match the referenced trace") if _draft_hash(trace_draft) != effective_draft.get("draftHash"): raise ValueError("MPS trace draft snapshot is inconsistent") linked_schedule_id = trace.get("scheduleId") linked_track = str(trace.get("scheduleTrack") or "").lower() if ( schedule_id is not None and linked_schedule_id is not None and not _same_id(linked_schedule_id, schedule_id) ): raise ValueError("MPS scheduleId does not match the referenced trace") if schedule_id is None: schedule_id = linked_schedule_id if tr == "auto": tr = linked_track or "fixed" elif linked_track and tr != linked_track: raise ValueError("MPS schedule track does not match the referenced trace") elif tr == "auto": tr = "fixed" if tr not in ("fixed", "flex"): raise ValueError("MPS schedule track must be fixed, flex, or auto") version = _resolve_schedule_version(world, tr, schedule_id) if trace and trace.get("scheduleId") is not None: if version is None: raise ValueError("MPS linked schedule version no longer exists") if str(version.get("versionNo") or "") != str( trace.get("scheduleVersionNo") or "" ): raise ValueError("MPS linked schedule version number has drifted") expected_schedule_fingerprint = str( trace.get("scheduleFingerprint") or "" ) if not expected_schedule_fingerprint: raise ValueError("MPS linked schedule is missing its fingerprint") if schedule_fingerprint(world, tr, version) != expected_schedule_fingerprint: raise ValueError("MPS linked schedule fingerprint has drifted") summary = effective_draft.get("summary") or {} capacity = effective_draft.get("capacity") or {} plan_kpi = { "weightedDemand": round(float(summary.get("weightedDemand") or 0), 1), "lineCount": int(summary.get("lineCount") or 0), "overallLoad": float(summary.get("overallLoad") or 0), "feasibleRate": float(capacity.get("feasibleRate") or 0), "overCount": int(summary.get("overCount") or 0), "warnCount": int(summary.get("warnCount") or 0), "peakLoadRatio": float(capacity.get("peakLoadRatio") or 0), "horizonDays": int(effective_draft.get("horizonDays") or 0), } if version is None: return { "track": tr, "plan": plan_kpi, "schedule": None, "gaps": None, "rows": [], "trace": { "planId": (trace or {}).get("planId"), "scheduleId": schedule_id, "scheduleVersionNo": (trace or {}).get("scheduleVersionNo"), "scheduleTrack": (trace or {}).get("scheduleTrack"), "scheduleFingerprint": (trace or {}).get("scheduleFingerprint"), "adoptId": (trace or {}).get("adoptId"), "publishId": (trace or {}).get("publishId"), "feedbackCount": len((trace or {}).get("executionFeedback") or []), }, "message": "No schedule version available for detailed KPI comparison.", } schedule_kpi = _schedule_kpi(world, tr, version) planned_qty = _planned_quantity(world, tr, version) gaps = { "demandQty": plan_kpi["weightedDemand"], "plannedQty": round(planned_qty, 1), "demandGapQty": round(plan_kpi["weightedDemand"] - planned_qty, 1), "loadRatioGap": round( plan_kpi["overallLoad"] - schedule_kpi["avgUtilization"], 3 ), "feasibleRateGap": round( plan_kpi["feasibleRate"] - (schedule_kpi["onTimeRate"] or 0), 3 ), "riskCountGap": max( 0, plan_kpi["overCount"] + plan_kpi["warnCount"] - schedule_kpi["conflictCount"], ), } rows = [ { "key": "demandQty", "name": "weighted demand qty", "plan": plan_kpi["weightedDemand"], "schedule": round(planned_qty, 1), "delta": gaps["demandGapQty"], "lowerBetter": False, }, { "key": "loadRatio", "name": "load ratio", "plan": plan_kpi["overallLoad"], "schedule": schedule_kpi["avgUtilization"], "delta": gaps["loadRatioGap"], "lowerBetter": True, }, { "key": "feasibleRate", "name": "feasible rate", "plan": plan_kpi["feasibleRate"], "schedule": schedule_kpi["onTimeRate"], "delta": gaps["feasibleRateGap"], "lowerBetter": False, }, { "key": "riskCount", "name": "risk count", "plan": plan_kpi["overCount"] + plan_kpi["warnCount"], "schedule": schedule_kpi["conflictCount"], "delta": gaps["riskCountGap"], "lowerBetter": True, }, ] message = ( f"Plan demand {plan_kpi['weightedDemand']} vs scheduled qty {round(planned_qty, 1)}; " f"rough-cut feasible rate {plan_kpi['feasibleRate']} vs detailed on-time rate " f"{schedule_kpi['onTimeRate']}." ) return { "track": tr, "plan": plan_kpi, "schedule": schedule_kpi, "gaps": gaps, "rows": rows, "trace": { "planId": (trace or {}).get("planId"), "scheduleId": schedule_id, "scheduleVersionNo": (trace or {}).get("scheduleVersionNo"), "scheduleTrack": (trace or {}).get("scheduleTrack"), "scheduleFingerprint": (trace or {}).get("scheduleFingerprint"), "adoptId": (trace or {}).get("adoptId"), "publishId": (trace or {}).get("publishId"), "feedbackCount": len((trace or {}).get("executionFeedback") or []), }, "message": message, }