""" RunManifest for execution provenance tracking. Implements FR-5: RunManifest Persistence Complete record of execution with all inputs, outputs, and metadata. """ from dataclasses import dataclass, field from datetime import datetime from typing import Dict, Any, List, Optional @dataclass class ResolvedInput: """ Record of a resolved input artifact. Attributes: name: Artifact name artifact_id: Artifact ID space_id: Space where artifact was found digest: Content digest """ name: str artifact_id: str space_id: str digest: str def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "name": self.name, "artifact_id": self.artifact_id, "space_id": self.space_id, "digest": self.digest, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "ResolvedInput": """Create from dictionary.""" return cls( name=data["name"], artifact_id=data["artifact_id"], space_id=data["space_id"], digest=data["digest"], ) @dataclass class DependencyEdge: """ Dependency edge in execution graph. Attributes: source_id: Source artifact/run ID target_id: Target artifact/run ID edge_type: Type of dependency (requires, generates, includes) """ source_id: str target_id: str edge_type: str def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "source_id": self.source_id, "target_id": self.target_id, "edge_type": self.edge_type, } @dataclass class OutputArtifact: """ Artifact produced by execution. Attributes: artifact_id: Artifact ID name: Artifact name digest: Content digest artifact_type: Type of artifact """ artifact_id: str name: str digest: str artifact_type: str def to_dict(self) -> Dict[str, Any]: """Convert to dictionary.""" return { "artifact_id": self.artifact_id, "name": self.name, "digest": self.digest, "artifact_type": self.artifact_type, } @dataclass class RunManifest: """ Complete execution manifest with provenance. Implements FR-5: RunManifest Persistence The RunManifest provides complete traceability for a prompt execution, capturing all inputs, outputs, configuration, and metadata. Implements FR-5.2: RunManifest contents: - Template metadata - Resolved inputs and their digests - CompiledPrompt digest - Model configuration - Output artifacts and digests - Dependency edges - Validation results - Impact debt records (if applicable) Attributes: run_id: ID of associated run template_metadata: Template information resolved_inputs: List of resolved input artifacts compiled_prompt_digest: Digest of compiled prompt model_config: Model configuration used output_artifacts: List of produced artifacts dependency_edges: Dependency graph edges validation_results: Quality validation results impact_debt: Suppressed recomputation records timing_metadata: Execution timing information created_at: Manifest creation time """ run_id: str template_metadata: Dict[str, Any] resolved_inputs: List[ResolvedInput] = field(default_factory=list) compiled_prompt_digest: str = "" model_config: Dict[str, Any] = field(default_factory=dict) output_artifacts: List[OutputArtifact] = field(default_factory=list) dependency_edges: List[DependencyEdge] = field(default_factory=list) validation_results: Dict[str, Any] = field(default_factory=dict) impact_debt: List[Dict[str, Any]] = field(default_factory=list) timing_metadata: Dict[str, float] = field(default_factory=dict) created_at: datetime = field(default_factory=datetime.utcnow) @classmethod def create( cls, run_id: str, template_id: str, template_name: str, template_digest: str, ) -> "RunManifest": """ Create a new manifest. Args: run_id: Run ID template_id: Template ID template_name: Template name template_digest: Template content digest Returns: New RunManifest instance """ return cls( run_id=run_id, template_metadata={ "template_id": template_id, "template_name": template_name, "template_digest": template_digest, }, ) def add_resolved_input( self, name: str, artifact_id: str, space_id: str, digest: str, ) -> None: """ Add a resolved input artifact. Args: name: Artifact name artifact_id: Artifact ID space_id: Space ID digest: Content digest """ self.resolved_inputs.append( ResolvedInput( name=name, artifact_id=artifact_id, space_id=space_id, digest=digest, ) ) def add_output_artifact( self, artifact_id: str, name: str, digest: str, artifact_type: str, ) -> None: """ Add an output artifact. Args: artifact_id: Artifact ID name: Artifact name digest: Content digest artifact_type: Artifact type """ self.output_artifacts.append( OutputArtifact( artifact_id=artifact_id, name=name, digest=digest, artifact_type=artifact_type, ) ) def add_dependency_edge( self, source_id: str, target_id: str, edge_type: str, ) -> None: """ Add a dependency edge. Args: source_id: Source ID target_id: Target ID edge_type: Edge type """ self.dependency_edges.append( DependencyEdge( source_id=source_id, target_id=target_id, edge_type=edge_type, ) ) def set_timing(self, stage: str, duration_seconds: float) -> None: """ Record timing for a stage. Args: stage: Stage name duration_seconds: Duration in seconds """ self.timing_metadata[stage] = duration_seconds def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "run_id": self.run_id, "template_metadata": self.template_metadata, "resolved_inputs": [inp.to_dict() for inp in self.resolved_inputs], "compiled_prompt_digest": self.compiled_prompt_digest, "model_config": self.model_config, "output_artifacts": [out.to_dict() for out in self.output_artifacts], "dependency_edges": [edge.to_dict() for edge in self.dependency_edges], "validation_results": self.validation_results, "impact_debt": self.impact_debt, "timing_metadata": self.timing_metadata, "created_at": self.created_at.isoformat(), } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "RunManifest": """Create from dictionary.""" return cls( run_id=data["run_id"], template_metadata=data["template_metadata"], resolved_inputs=[ ResolvedInput.from_dict(inp) for inp in data.get("resolved_inputs", []) ], compiled_prompt_digest=data.get("compiled_prompt_digest", ""), model_config=data.get("model_config", {}), output_artifacts=[ OutputArtifact(**out) for out in data.get("output_artifacts", []) ], dependency_edges=[ DependencyEdge(**edge) for edge in data.get("dependency_edges", []) ], validation_results=data.get("validation_results", {}), impact_debt=data.get("impact_debt", []), timing_metadata=data.get("timing_metadata", {}), created_at=datetime.fromisoformat(data["created_at"]), )