Coverage for src/prosemark/domain/materialize_failure.py: 100%
39 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""MaterializeFailure value object for failed placeholder materialization."""
3from dataclasses import dataclass
4from typing import ClassVar
7@dataclass(frozen=True)
8class MaterializeFailure:
9 """Represents a failed placeholder materialization attempt.
11 This value object captures information about why a placeholder
12 materialization failed, providing context for error reporting and recovery.
14 Args:
15 display_title: Title of the placeholder that failed to materialize
16 error_type: Type of error (filesystem, validation, etc.)
17 error_message: Human-readable error description
18 position: Position in binder hierarchy where failure occurred
20 Raises:
21 ValueError: If validation rules are violated during construction
23 """
25 display_title: str
26 error_type: str
27 error_message: str
28 position: str | None
30 # Valid error types
31 VALID_ERROR_TYPES: ClassVar[set[str]] = {
32 'filesystem', # File creation, permission, disk space issues
33 'validation', # Invalid placeholder state, corrupted binder
34 'already_materialized', # Placeholder already has node_id
35 'binder_integrity', # Binder structure violations
36 'id_generation', # UUID generation failures
37 }
39 def __post_init__(self) -> None:
40 """Validate the materialization failure after construction."""
41 # Validate display_title is non-empty
42 if not self.display_title or not self.display_title.strip():
43 msg = 'Display title must be non-empty string'
44 raise ValueError(msg)
46 # Validate error_type is from predefined set
47 if self.error_type not in self.VALID_ERROR_TYPES:
48 msg = f"Error type must be one of {sorted(self.VALID_ERROR_TYPES)}, got '{self.error_type}'"
49 raise ValueError(msg)
51 # Validate error_message is non-empty and human-readable
52 if not self.error_message or not self.error_message.strip():
53 msg = 'Error message must be non-empty and human-readable'
54 raise ValueError(msg)
56 # Position validation is optional since it might not be available in all error contexts
57 # but if provided, should not be empty
58 if self.position is not None and not self.position.strip():
59 msg = 'Position must be None or non-empty string'
60 raise ValueError(msg)
62 @property
63 def is_retryable(self) -> bool:
64 """Check if this type of error might be retryable by the user."""
65 retryable_types = {'filesystem'} # User might fix permissions, free disk space, etc.
66 return self.error_type in retryable_types
68 @property
69 def is_critical(self) -> bool:
70 """Check if this error indicates a critical system problem."""
71 critical_types = {'binder_integrity', 'id_generation'}
72 return self.error_type in critical_types
74 @property
75 def should_stop_batch(self) -> bool:
76 """Check if this error should stop the entire batch operation."""
77 # Critical errors should stop the batch to prevent data corruption
78 return self.is_critical
80 def formatted_error(self) -> str:
81 """Generate formatted error message for display."""
82 if self.position:
83 return f"✗ Failed to materialize '{self.display_title}' at {self.position}: {self.error_message}"
84 return f"✗ Failed to materialize '{self.display_title}': {self.error_message}"
86 def __str__(self) -> str:
87 """Generate human-readable string representation."""
88 return self.formatted_error()