""" Data models for incremental execution. Implements FR-7: Incremental Recomputation Implements FR-8: Impact Analysis Provides ChangeType enum, ArtifactChange, ImpactDebt, RecomputeConfig, and RecomputeResult dataclasses. """ import uuid from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import Any, Dict, List, Optional class ChangeType(Enum): """Type of artifact change detected.""" CREATED = "created" MODIFIED = "modified" DELETED = "deleted" @dataclass class ArtifactChange: """ Record of a detected artifact change. Attributes: id: Unique change identifier artifact_id: ID of changed artifact old_digest: Previous content digest (None for created) new_digest: New content digest change_type: Type of change detected_at: When change was detected """ id: str artifact_id: str old_digest: Optional[str] new_digest: str change_type: ChangeType detected_at: datetime = field(default_factory=datetime.utcnow) @classmethod def create( cls, artifact_id: str, old_digest: Optional[str], new_digest: str, change_type: ChangeType, ) -> "ArtifactChange": """ Create a new ArtifactChange record. Args: artifact_id: ID of changed artifact old_digest: Previous digest (None for created) new_digest: New digest change_type: Type of change Returns: New ArtifactChange instance """ return cls( id=str(uuid.uuid4()), artifact_id=artifact_id, old_digest=old_digest, new_digest=new_digest, change_type=change_type, ) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "id": self.id, "artifact_id": self.artifact_id, "old_digest": self.old_digest, "new_digest": self.new_digest, "change_type": self.change_type.value, "detected_at": self.detected_at.isoformat(), } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "ArtifactChange": """Create from dictionary.""" return cls( id=data["id"], artifact_id=data["artifact_id"], old_digest=data.get("old_digest"), new_digest=data["new_digest"], change_type=ChangeType(data["change_type"]), detected_at=datetime.fromisoformat(data["detected_at"]), ) @dataclass class ImpactDebt: """ Record of a suppressed recomputation. When a dependent is not recomputed (due to circular dependency, below-threshold impact, or budget exhaustion), an ImpactDebt record is created to track the outstanding work. Attributes: id: Unique debt identifier artifact_id: ID of the changed artifact that triggered this dependent_run_id: Run ID of the dependent that was not recomputed change_magnitude: Measured impact magnitude (0.0-1.0) suppression_reason: Why recomputation was suppressed recorded_at: When debt was recorded """ id: str artifact_id: str dependent_run_id: str change_magnitude: float suppression_reason: str recorded_at: datetime = field(default_factory=datetime.utcnow) @classmethod def create( cls, artifact_id: str, dependent_run_id: str, change_magnitude: float, suppression_reason: str, ) -> "ImpactDebt": """ Create a new ImpactDebt record. Args: artifact_id: Changed artifact ID dependent_run_id: Dependent run ID change_magnitude: Impact magnitude (0.0-1.0) suppression_reason: Reason for suppression Returns: New ImpactDebt instance """ return cls( id=str(uuid.uuid4()), artifact_id=artifact_id, dependent_run_id=dependent_run_id, change_magnitude=change_magnitude, suppression_reason=suppression_reason, ) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "id": self.id, "artifact_id": self.artifact_id, "dependent_run_id": self.dependent_run_id, "change_magnitude": self.change_magnitude, "suppression_reason": self.suppression_reason, "recorded_at": self.recorded_at.isoformat(), } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "ImpactDebt": """Create from dictionary.""" return cls( id=data["id"], artifact_id=data["artifact_id"], dependent_run_id=data["dependent_run_id"], change_magnitude=data["change_magnitude"], suppression_reason=data["suppression_reason"], recorded_at=datetime.fromisoformat(data["recorded_at"]), ) @dataclass class RecomputeConfig: """ Configuration for incremental recomputation. Attributes: max_depth: Maximum BFS depth for finding dependents (FR-7.2) impact_threshold: Minimum magnitude to trigger recompute (FR-8.2) max_recomputes: Budget limit on recomputations per change suppress_circular: Whether to suppress circular dependencies """ max_depth: int = 1 impact_threshold: float = 0.0 max_recomputes: int = 100 suppress_circular: bool = True def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "max_depth": self.max_depth, "impact_threshold": self.impact_threshold, "max_recomputes": self.max_recomputes, "suppress_circular": self.suppress_circular, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "RecomputeConfig": """Create from dictionary.""" return cls( max_depth=data.get("max_depth", 1), impact_threshold=data.get("impact_threshold", 0.0), max_recomputes=data.get("max_recomputes", 100), suppress_circular=data.get("suppress_circular", True), ) @dataclass class RecomputeResult: """ Result of an incremental recomputation triggered by a change. Attributes: changed_artifact_id: ID of the artifact that changed change: The detected change record executed_run_ids: Run IDs that were recomputed suppressed: List of suppressed recomputation debt records total_dependents: Total number of dependents found recomputed_count: Number actually recomputed suppressed_count: Number suppressed """ changed_artifact_id: str change: ArtifactChange executed_run_ids: List[str] = field(default_factory=list) suppressed: List[ImpactDebt] = field(default_factory=list) total_dependents: int = 0 recomputed_count: int = 0 suppressed_count: int = 0 def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "changed_artifact_id": self.changed_artifact_id, "change": self.change.to_dict(), "executed_run_ids": self.executed_run_ids, "suppressed": [d.to_dict() for d in self.suppressed], "total_dependents": self.total_dependents, "recomputed_count": self.recomputed_count, "suppressed_count": self.suppressed_count, }