Coverage for src / moai_adk / core / git / checkpoint.py: 29.27%

41 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-20 20:52 +0900

1""" 

2Checkpoint Manager - Event-driven checkpoint system. 

3 

4SPEC: .moai/specs/SPEC-CHECKPOINT-EVENT-001/spec.md 

5""" 

6 

7from datetime import datetime 

8from pathlib import Path 

9from typing import Optional 

10 

11import git 

12 

13from moai_adk.core.git.branch_manager import BranchManager 

14from moai_adk.core.git.event_detector import EventDetector 

15 

16 

17class CheckpointManager: 

18 """Manage creation and restoration of event-driven checkpoints.""" 

19 

20 def __init__(self, repo: git.Repo, project_root: Path): 

21 """ 

22 Initialize the CheckpointManager. 

23 

24 Args: 

25 repo: GitPython Repo instance. 

26 project_root: Project root directory. 

27 """ 

28 self.repo = repo 

29 self.project_root = project_root 

30 self.event_detector = EventDetector() 

31 self.branch_manager = BranchManager(repo) 

32 self.log_file = project_root / ".moai" / "checkpoints.log" 

33 

34 # Ensure the log directory exists 

35 self.log_file.parent.mkdir(parents=True, exist_ok=True) 

36 

37 def create_checkpoint_if_risky( 

38 self, 

39 operation: str, 

40 deleted_files: Optional[list[str]] = None, 

41 renamed_files: Optional[list[tuple[str, str]]] = None, 

42 modified_files: Optional[list[Path]] = None, 

43 ) -> Optional[str]: 

44 """ 

45 Create a checkpoint when a risky operation is detected. 

46 

47 SPEC requirement: automatically create a checkpoint for risky actions. 

48 

49 Args: 

50 operation: Operation type. 

51 deleted_files: Files scheduled for deletion. 

52 renamed_files: Files that will be renamed. 

53 modified_files: Files that will be modified. 

54 

55 Returns: 

56 Created checkpoint ID (branch name) or None when the operation is safe. 

57 """ 

58 is_risky = False 

59 

60 # Identify large deletion operations 

61 if deleted_files and self.event_detector.is_risky_deletion(deleted_files): 

62 is_risky = True 

63 

64 # Identify large-scale refactoring 

65 if renamed_files and self.event_detector.is_risky_refactoring(renamed_files): 

66 is_risky = True 

67 

68 # Check for critical file modifications 

69 if modified_files: 

70 for file_path in modified_files: 

71 if self.event_detector.is_critical_file(file_path): 

72 is_risky = True 

73 break 

74 

75 if not is_risky: 

76 return None 

77 

78 # Create a checkpoint 

79 checkpoint_id = self.branch_manager.create_checkpoint_branch(operation) 

80 

81 # Record checkpoint metadata 

82 self._log_checkpoint(checkpoint_id, operation) 

83 

84 return checkpoint_id 

85 

86 def restore_checkpoint(self, checkpoint_id: str) -> None: 

87 """ 

88 Restore the repository to the specified checkpoint. 

89 

90 SPEC requirement: capture the current state as a new checkpoint before restoring. 

91 

92 Args: 

93 checkpoint_id: Target checkpoint ID (branch name). 

94 """ 

95 # Save current state as a safety checkpoint before restoring 

96 safety_checkpoint = self.branch_manager.create_checkpoint_branch("restore") 

97 self._log_checkpoint(safety_checkpoint, "restore", is_safety=True) 

98 

99 # Check out the checkpoint branch 

100 self.repo.git.checkout(checkpoint_id) 

101 

102 def list_checkpoints(self) -> list[str]: 

103 """ 

104 List all checkpoints. 

105 

106 Returns: 

107 List of checkpoint IDs. 

108 """ 

109 return self.branch_manager.list_checkpoint_branches() 

110 

111 def _log_checkpoint( 

112 self, checkpoint_id: str, operation: str, is_safety: bool = False 

113 ) -> None: 

114 """ 

115 Append checkpoint metadata to the log file. 

116 

117 SPEC requirement: write metadata to .moai/checkpoints.log. 

118 

119 Args: 

120 checkpoint_id: Checkpoint identifier. 

121 operation: Operation type. 

122 is_safety: Whether the checkpoint was created for safety. 

123 """ 

124 timestamp = datetime.now().isoformat() 

125 

126 log_entry = f"""--- 

127checkpoint_id: {checkpoint_id} 

128operation: {operation} 

129timestamp: {timestamp} 

130is_safety: {is_safety} 

131--- 

132""" 

133 

134 # Append the entry to the log 

135 with open(self.log_file, "a", encoding="utf-8") as f: 

136 f.write(log_entry)