"""Deterministic snapshots and replay helpers for CrisisOps.""" from __future__ import annotations import hashlib import json from typing import Any, Dict, Iterable, List from .core import DualVariables, GraphOfContracts, Morphism, Object, PlanDelta SCHEMA_VERSION = "1.0" def canonical_json(payload: Any) -> str: return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) def _numeric(value: Any, default: float = 0.0) -> float: try: return float(value) except (TypeError, ValueError): return default def _action_sort_key(action: Dict[str, Any]) -> tuple: return ( str(action.get("morphism_id", "")), str(action.get("action", "")), str(action.get("resource_id", "")), str(action.get("destination_id", "")), ) def _object_to_dict(item: Object) -> Dict[str, Any]: return {"id": item.id, "type": item.type, "attributes": dict(item.attributes)} def _morphism_to_dict(item: Morphism) -> Dict[str, Any]: return { "id": item.id, "source_id": item.source_id, "target_id": item.target_id, "type": item.type, "signals": dict(item.signals), } def _dual_to_dict(item: DualVariables) -> Dict[str, Any]: return {"name": item.name, "values": dict(item.values)} def _plan_to_dict(plan: PlanDelta | None) -> Dict[str, Any] | None: if plan is None: return None actions = [dict(action) for action in plan.actions] actions.sort(key=_action_sort_key) return {"timestamp": plan.timestamp, "actions": actions} def graph_snapshot(graph: GraphOfContracts, plan: PlanDelta | None = None) -> Dict[str, Any]: payload = { "schema_version": SCHEMA_VERSION, "objects": [_object_to_dict(graph.objects[key]) for key in sorted(graph.objects)], "morphisms": [_morphism_to_dict(graph.morphisms[key]) for key in sorted(graph.morphisms)], "duals": [_dual_to_dict(graph.duals[key]) for key in sorted(graph.duals)], "plan_delta": _plan_to_dict(plan or graph.plan_delta), } payload["snapshot_hash"] = snapshot_hash(payload) return payload def snapshot_hash(snapshot: Dict[str, Any]) -> str: normalized = dict(snapshot) normalized.pop("snapshot_hash", None) return hashlib.sha256(canonical_json(normalized).encode("utf-8")).hexdigest() def restore_graph(snapshot: Dict[str, Any]) -> GraphOfContracts: objects = { item["id"]: Object(id=item["id"], type=item["type"], attributes=dict(item.get("attributes", {}))) for item in snapshot.get("objects", []) } morphisms = { item["id"]: Morphism( id=item["id"], source_id=item["source_id"], target_id=item["target_id"], type=item["type"], signals=dict(item.get("signals", {})), ) for item in snapshot.get("morphisms", []) } duals = { item["name"]: DualVariables(name=item["name"], values=dict(item.get("values", {}))) for item in snapshot.get("duals", []) } plan_data = snapshot.get("plan_delta") plan_delta = None if plan_data is not None: plan_delta = PlanDelta( timestamp=_numeric(plan_data.get("timestamp")), actions=[dict(action) for action in plan_data.get("actions", [])], ) graph = GraphOfContracts(objects=objects, morphisms=morphisms, plan_delta=plan_delta, duals=duals) return graph def replay_graphs(snapshots: Iterable[Dict[str, Any]]) -> List[GraphOfContracts]: return [restore_graph(snapshot) for snapshot in snapshots] class ReplayJournal: """A minimal append-only journal of canonical graph snapshots.""" def __init__(self) -> None: self._snapshots: List[Dict[str, Any]] = [] def record(self, graph: GraphOfContracts, plan: PlanDelta | None = None) -> Dict[str, Any]: snapshot = graph_snapshot(graph, plan=plan) self._snapshots.append(snapshot) return snapshot def list_snapshots(self) -> List[Dict[str, Any]]: return [dict(snapshot) for snapshot in self._snapshots] def replay(self) -> List[GraphOfContracts]: return replay_graphs(self._snapshots)