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

1"""MaterializeFailure value object for failed placeholder materialization.""" 

2 

3from dataclasses import dataclass 

4from typing import ClassVar 

5 

6 

7@dataclass(frozen=True) 

8class MaterializeFailure: 

9 """Represents a failed placeholder materialization attempt. 

10 

11 This value object captures information about why a placeholder 

12 materialization failed, providing context for error reporting and recovery. 

13 

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 

19 

20 Raises: 

21 ValueError: If validation rules are violated during construction 

22 

23 """ 

24 

25 display_title: str 

26 error_type: str 

27 error_message: str 

28 position: str | None 

29 

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 } 

38 

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) 

45 

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) 

50 

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) 

55 

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) 

61 

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 

67 

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 

73 

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 

79 

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

85 

86 def __str__(self) -> str: 

87 """Generate human-readable string representation.""" 

88 return self.formatted_error()