from dataclasses import dataclass, asdict from typing import List, Dict, Any, Optional import json import hashlib from .contracts import Environment @dataclass class ReproBundle: """A compact, content-addressed snapshot describing everything needed to deterministically replay a run (code commit, environment, dataset refs, model ref, and a minimal run manifest). This is intentionally small and deterministic so it can be cheaply exchanged between peers and used to compute Merkle-like bundle hashes. """ code_commit: str environment: Environment dataset_refs: List[Dict[str, Any]] model_ref: Dict[str, Any] run_manifest: Dict[str, Any] container_hash: Optional[str] = None def to_dict(self) -> Dict[str, Any]: # Convert to a canonical dict suitable for hashing/serialization. return { "code_commit": self.code_commit, "environment": self.environment.to_dict(), "container_hash": self.container_hash, "dataset_refs": sorted(self.dataset_refs, key=lambda d: d.get("id") or ""), "model_ref": self.model_ref, "run_manifest": self.run_manifest, } def compute_bundle_hash(self) -> str: """Compute a deterministic SHA-256 digest over the canonical bundle representation. Uses JSON with sorted keys so identical bundles always produce the same digest. """ payload = self.to_dict() serialized = json.dumps(payload, sort_keys=True, default=str).encode("utf-8") return hashlib.sha256(serialized).hexdigest()