Files
markitect-main/markitect/prompts/incremental/models.py
tegwick bd1d05ba79 feat(prompts): implement Phase 6 - Incremental Execution (FR-7, FR-8)
Add change detection, structural diff-based impact analysis,
configurable-depth incremental recomputation with circular suppression,
and impact debt tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:18:27 +01:00

242 lines
7.7 KiB
Python

"""
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,
}