crackerjack 0.31.9__py3-none-any.whl → 0.31.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +282 -95
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +355 -204
  41. crackerjack/dynamic_config.py +47 -6
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +52 -62
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +51 -76
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +78 -44
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +281 -433
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.12.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.9.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
- import re
2
1
  from pathlib import Path
3
2
 
3
+ from ..services.regex_patterns import SAFE_PATTERNS
4
4
  from .base import (
5
5
  AgentContext,
6
6
  FixResult,
@@ -15,14 +15,14 @@ class TestSpecialistAgent(SubAgent):
15
15
  def __init__(self, context: AgentContext) -> None:
16
16
  super().__init__(context)
17
17
  self.common_test_patterns = {
18
- "fixture_not_found": r"fixture '(\w+)' not found",
19
- "import_error": r"ImportError|ModuleNotFoundError",
20
- "assertion_error": r"AssertionError|assert .+ ==",
21
- "attribute_error": r"AttributeError: .+ has no attribute",
22
- "mock_spec_error": r"MockSpec|spec.*Mock",
23
- "hardcoded_path": r"'/test/path'|/test/path",
24
- "missing_import": r"name '(\w+)' is not defined",
25
- "pydantic_validation": r"ValidationError|validation error",
18
+ "fixture_not_found": SAFE_PATTERNS["fixture_not_found_pattern"].pattern,
19
+ "import_error": SAFE_PATTERNS["import_error_pattern"].pattern,
20
+ "assertion_error": SAFE_PATTERNS["assertion_error_pattern"].pattern,
21
+ "attribute_error": SAFE_PATTERNS["attribute_error_pattern"].pattern,
22
+ "mock_spec_error": SAFE_PATTERNS["mock_spec_error_pattern"].pattern,
23
+ "hardcoded_path": SAFE_PATTERNS["hardcoded_path_pattern"].pattern,
24
+ "missing_import": SAFE_PATTERNS["missing_name_pattern"].pattern,
25
+ "pydantic_validation": SAFE_PATTERNS["pydantic_validation_pattern"].pattern,
26
26
  }
27
27
 
28
28
  def get_supported_types(self) -> set[IssueType]:
@@ -63,9 +63,27 @@ class TestSpecialistAgent(SubAgent):
63
63
  )
64
64
 
65
65
  def _check_test_patterns(self, message: str) -> float:
66
+ # Map pattern strings back to SAFE_PATTERNS for safe usage
67
+ pattern_map = {
68
+ SAFE_PATTERNS[
69
+ "fixture_not_found_pattern"
70
+ ].pattern: "fixture_not_found_pattern",
71
+ SAFE_PATTERNS["import_error_pattern"].pattern: "import_error_pattern",
72
+ SAFE_PATTERNS["assertion_error_pattern"].pattern: "assertion_error_pattern",
73
+ SAFE_PATTERNS["attribute_error_pattern"].pattern: "attribute_error_pattern",
74
+ SAFE_PATTERNS["mock_spec_error_pattern"].pattern: "mock_spec_error_pattern",
75
+ SAFE_PATTERNS["hardcoded_path_pattern"].pattern: "hardcoded_path_pattern",
76
+ SAFE_PATTERNS["missing_name_pattern"].pattern: "missing_name_pattern",
77
+ SAFE_PATTERNS[
78
+ "pydantic_validation_pattern"
79
+ ].pattern: "pydantic_validation_pattern",
80
+ }
81
+
66
82
  for pattern in self.common_test_patterns.values():
67
- if re.search(pattern, message, re.IGNORECASE):
68
- return 0.9
83
+ if pattern in pattern_map:
84
+ safe_pattern = SAFE_PATTERNS[pattern_map[pattern]]
85
+ if safe_pattern.test(message):
86
+ return 0.9
69
87
  return 0.0
70
88
 
71
89
  def _check_test_file_path(self, file_path: str | None) -> float:
@@ -173,16 +191,40 @@ class TestSpecialistAgent(SubAgent):
173
191
  def _identify_failure_type(self, issue: Issue) -> str:
174
192
  message = issue.message
175
193
 
194
+ # Map pattern strings back to SAFE_PATTERNS for safe usage
195
+ pattern_map = {
196
+ SAFE_PATTERNS[
197
+ "fixture_not_found_pattern"
198
+ ].pattern: "fixture_not_found_pattern",
199
+ SAFE_PATTERNS["import_error_pattern"].pattern: "import_error_pattern",
200
+ SAFE_PATTERNS["assertion_error_pattern"].pattern: "assertion_error_pattern",
201
+ SAFE_PATTERNS["attribute_error_pattern"].pattern: "attribute_error_pattern",
202
+ SAFE_PATTERNS["mock_spec_error_pattern"].pattern: "mock_spec_error_pattern",
203
+ SAFE_PATTERNS["hardcoded_path_pattern"].pattern: "hardcoded_path_pattern",
204
+ SAFE_PATTERNS["missing_name_pattern"].pattern: "missing_name_pattern",
205
+ SAFE_PATTERNS[
206
+ "pydantic_validation_pattern"
207
+ ].pattern: "pydantic_validation_pattern",
208
+ }
209
+
176
210
  for pattern_name, pattern in self.common_test_patterns.items():
177
- if re.search(pattern, message, re.IGNORECASE):
178
- return pattern_name
211
+ if pattern in pattern_map:
212
+ safe_pattern = SAFE_PATTERNS[pattern_map[pattern]]
213
+ if safe_pattern.test(message):
214
+ return pattern_name
179
215
 
180
216
  return "unknown"
181
217
 
182
218
  async def _fix_missing_fixtures(self, issue: Issue) -> list[str]:
183
219
  fixes: list[str] = []
184
220
 
185
- match = re.search(r"fixture '(\w+)' not found", issue.message)
221
+ # Use safe pattern to test and extract fixture name
222
+ fixture_pattern = SAFE_PATTERNS["fixture_not_found_pattern"]
223
+ if not fixture_pattern.test(issue.message):
224
+ return fixes
225
+
226
+ # Extract fixture name using the safe pattern's search method
227
+ match = fixture_pattern.search(issue.message)
186
228
  if not match:
187
229
  return fixes
188
230
 
@@ -462,7 +504,9 @@ def console() -> Console:
462
504
  content = "\n".join(lines)
463
505
  fixes.append(f"Added pytest import to {file_path}")
464
506
 
465
- content = re.sub(r"assert (.+) == (.+)", r"assert \1 == \2", content)
507
+ from crackerjack.services.regex_patterns import apply_test_fixes
508
+
509
+ content = apply_test_fixes(content)
466
510
 
467
511
  if content != original_content:
468
512
  if self.context.write_file_content(path, content):
@@ -2,7 +2,6 @@ import time
2
2
  import typing as t
3
3
  from collections import defaultdict
4
4
  from dataclasses import dataclass, field
5
- from typing import Any
6
5
 
7
6
  from .base import FixResult, Issue
8
7
 
@@ -42,19 +41,6 @@ class AgentTracker:
42
41
  "agent_types": agent_types,
43
42
  }
44
43
 
45
- def track_agent_evaluation(
46
- self,
47
- agent_type: str,
48
- issue: Issue,
49
- confidence: float,
50
- ) -> None:
51
- self.active_agents[agent_type] = AgentActivity(
52
- agent_type=agent_type,
53
- confidence=confidence,
54
- status="evaluating",
55
- current_issue=issue,
56
- )
57
-
58
44
  def track_agent_processing(
59
45
  self,
60
46
  agent_type: str,
@@ -89,94 +75,6 @@ class AgentTracker:
89
75
  self.completed_activities.append(activity)
90
76
  del self.active_agents[agent_type]
91
77
 
92
- def track_cache_hit(self) -> None:
93
- self.cache_stats["hits"] += 1
94
-
95
- def track_cache_miss(self) -> None:
96
- self.cache_stats["misses"] += 1
97
-
98
- def get_status(self) -> dict[str, Any]:
99
- active_agents: list[dict[str, Any]] = []
100
-
101
- for agent_type, activity in self.active_agents.items():
102
- agent_data: dict[str, Any] = {
103
- "agent_type": agent_type,
104
- "confidence": activity.confidence,
105
- "status": activity.status,
106
- "processing_time": time.time() - activity.start_time,
107
- "start_time": activity.start_time,
108
- }
109
-
110
- if activity.current_issue:
111
- agent_data["current_issue"] = {
112
- "type": activity.current_issue.type.value,
113
- "message": activity.current_issue.message,
114
- "priority": activity.current_issue.severity.value,
115
- "file_path": activity.current_issue.file_path,
116
- }
117
-
118
- active_agents.append(agent_data)
119
-
120
- return {
121
- "coordinator_status": self.coordinator_status,
122
- "active_agents": active_agents,
123
- "agent_registry": self.agent_registry.copy(),
124
- }
125
-
126
- def get_metrics(self) -> dict[str, Any]:
127
- total_completed = len(self.completed_activities)
128
- successful = sum(
129
- 1
130
- for activity in self.completed_activities
131
- if activity.result and activity.result.success
132
- )
133
- success_rate = successful / total_completed if total_completed > 0 else 0.0
134
-
135
- all_times: list[float] = []
136
- for times in self.performance_metrics.values():
137
- all_times.extend(times)
138
-
139
- avg_processing_time = sum(all_times) / len(all_times) if all_times else 0.0
140
-
141
- total_requests = self.cache_stats["hits"] + self.cache_stats["misses"]
142
- cache_hit_rate = (
143
- self.cache_stats["hits"] / total_requests if total_requests > 0 else 0.0
144
- )
145
-
146
- return {
147
- "total_issues_processed": self.total_issues_processed,
148
- "cache_hits": self.cache_stats["hits"],
149
- "cache_misses": self.cache_stats["misses"],
150
- "cache_hit_rate": cache_hit_rate,
151
- "average_processing_time": avg_processing_time,
152
- "success_rate": success_rate,
153
- "completed_activities": total_completed,
154
- }
155
-
156
- def get_agent_summary(self) -> dict[str, Any]:
157
- active_count = len(self.active_agents)
158
- cache_hits = self.cache_stats["hits"]
159
-
160
- active_summary: list[dict[str, Any]] = []
161
- for agent_type, activity in self.active_agents.items():
162
- emoji = self._get_agent_emoji(agent_type)
163
- processing_time = time.time() - activity.start_time
164
-
165
- active_summary.append(
166
- {
167
- "display": f"{emoji} {agent_type}: {activity.status.title()} ({processing_time:.1f}s)",
168
- "agent_type": agent_type,
169
- "status": activity.status,
170
- "processing_time": processing_time,
171
- },
172
- )
173
-
174
- return {
175
- "active_count": active_count,
176
- "cached_fixes": cache_hits,
177
- "active_agents": active_summary,
178
- }
179
-
180
78
  def _get_agent_emoji(self, agent_type: str) -> str:
181
79
  return {
182
80
  "FormattingAgent": "🎨",
crackerjack/api.py CHANGED
@@ -5,12 +5,13 @@ from pathlib import Path
5
5
 
6
6
  from rich.console import Console
7
7
 
8
- from .code_cleaner import CleaningResult, CodeCleaner
8
+ from .code_cleaner import CleaningResult, CodeCleaner, PackageCleaningResult
9
9
  from .core.workflow_orchestrator import WorkflowOrchestrator
10
10
  from .errors import CrackerjackError, ErrorCode
11
11
  from .interactive import InteractiveCLI
12
12
  from .interactive import WorkflowOptions as InteractiveWorkflowOptions
13
13
  from .models.config import WorkflowOptions
14
+ from .services.regex_patterns import SAFE_PATTERNS
14
15
 
15
16
 
16
17
  @dataclass
@@ -70,7 +71,9 @@ class CrackerjackAPI:
70
71
  @property
71
72
  def code_cleaner(self) -> CodeCleaner:
72
73
  if self._code_cleaner is None:
73
- self._code_cleaner = CodeCleaner(console=self.console)
74
+ self._code_cleaner = CodeCleaner(
75
+ console=self.console, base_directory=self.project_path
76
+ )
74
77
  return self._code_cleaner
75
78
 
76
79
  @property
@@ -126,18 +129,38 @@ class CrackerjackAPI:
126
129
  self,
127
130
  target_dir: Path | None = None,
128
131
  backup: bool = True,
129
- ) -> list[CleaningResult]:
130
- """Clean code with TODO validation and comprehensive error handling."""
131
- target_dir = target_dir or self.project_path
132
- self.logger.info(f"Cleaning code in {target_dir}")
132
+ safe_mode: bool = True,
133
+ ) -> list[CleaningResult] | PackageCleaningResult:
134
+ """Clean code with comprehensive backup protection.
135
+
136
+ Args:
137
+ target_dir: Directory to clean (defaults to package root)
138
+ backup: Whether to create backup (deprecated, always True for safety)
139
+ safe_mode: Use comprehensive backup system (default: True, recommended)
140
+
141
+ Returns:
142
+ PackageCleaningResult with backup metadata if safe_mode=True,
143
+ otherwise list[CleaningResult] for legacy compatibility
144
+ """
145
+ target_dir = target_dir or self._get_package_root()
146
+ self.logger.info(f"Cleaning code in {target_dir} (safe_mode={safe_mode})")
133
147
 
134
148
  self._validate_code_before_cleaning(target_dir)
135
- self._notify_backup_status(backup)
136
149
 
137
- return self._execute_code_cleaning(target_dir)
150
+ if safe_mode:
151
+ self.console.print(
152
+ "[green]🛡️ Using safe mode with comprehensive backup protection[/green]"
153
+ )
154
+ return self._execute_safe_code_cleaning(target_dir)
155
+ else:
156
+ # Note: Legacy mode still uses backup protection for safety
157
+ self.console.print(
158
+ "[yellow]⚠️ Legacy mode - backup protection still enabled for safety[/yellow]"
159
+ )
160
+ self._notify_backup_status(backup)
161
+ return self._execute_code_cleaning(target_dir)
138
162
 
139
163
  def _validate_code_before_cleaning(self, target_dir: Path) -> None:
140
- """Validate code state before cleaning, checking for TODOs."""
141
164
  todos_found = self._check_for_todos(target_dir)
142
165
  if todos_found:
143
166
  self._handle_todos_found(todos_found, target_dir)
@@ -147,11 +170,10 @@ class CrackerjackAPI:
147
170
  todos_found: list[tuple[Path, int, str]],
148
171
  target_dir: Path,
149
172
  ) -> None:
150
- """Handle case where TODOs are found in codebase."""
151
173
  todo_count = len(todos_found)
152
- self.console.print(f"[red]❌ Found {todo_count} TODO(s) in codebase[/red]")
174
+ self.console.print(f"[red]❌ Found {todo_count} TODO(s) in codebase[/ red]")
153
175
  self.console.print(
154
- "[yellow]Please resolve all TODOs before running code cleaning ( - x)[/yellow]",
176
+ "[yellow]Please resolve all TODOs before running code cleaning (-x)[/ yellow]",
155
177
  )
156
178
 
157
179
  self._display_todo_summary(todos_found, target_dir, todo_count)
@@ -167,7 +189,6 @@ class CrackerjackAPI:
167
189
  target_dir: Path,
168
190
  todo_count: int,
169
191
  ) -> None:
170
- """Display summary of found TODOs."""
171
192
  for _i, (file_path, line_no, content) in enumerate(todos_found[:5]):
172
193
  relative_path = file_path.relative_to(target_dir)
173
194
  self.console.print(f" {relative_path}: {line_no}: {content.strip()}")
@@ -176,33 +197,68 @@ class CrackerjackAPI:
176
197
  self.console.print(f" ... and {todo_count - 5} more")
177
198
 
178
199
  def _notify_backup_status(self, backup: bool) -> None:
179
- """Notify user about backup file creation."""
180
200
  if backup:
181
- self.console.print("[yellow]Note: Backup files will be created[/yellow]")
201
+ self.console.print("[yellow]Note: Backup files will be created[/ yellow]")
202
+
203
+ def _execute_safe_code_cleaning(self, target_dir: Path) -> PackageCleaningResult:
204
+ try:
205
+ result = self.code_cleaner.clean_files_with_backup(target_dir)
206
+ self._report_safe_cleaning_results(result)
207
+ return result
208
+ except Exception as e:
209
+ self._handle_cleaning_error(e)
182
210
 
183
211
  def _execute_code_cleaning(self, target_dir: Path) -> list[CleaningResult]:
184
- """Execute code cleaning and handle results."""
185
212
  try:
186
- results = self.code_cleaner.clean_files(target_dir)
187
- self._report_cleaning_results(results)
213
+ # Use backup protection by default for safety
214
+ results = self.code_cleaner.clean_files(target_dir, use_backup=True)
215
+
216
+ # Handle both return types (legacy compatibility)
217
+ if isinstance(results, list):
218
+ self._report_cleaning_results(results)
219
+ else:
220
+ # PackageCleaningResult from backup mode
221
+ self._report_safe_cleaning_results(results)
222
+ results = results.file_results # Extract list for compatibility
223
+
188
224
  return results
189
225
  except Exception as e:
190
226
  self._handle_cleaning_error(e)
191
227
 
228
+ def _report_safe_cleaning_results(self, result: PackageCleaningResult) -> None:
229
+ if result.overall_success:
230
+ self.console.print(
231
+ f"[green]🎉 Package cleaning completed successfully![/green] "
232
+ f"({result.successful_files}/{result.total_files} files cleaned)"
233
+ )
234
+ else:
235
+ self.console.print(
236
+ f"[red]❌ Package cleaning failed![/red] "
237
+ f"({result.failed_files}/{result.total_files} files failed)"
238
+ )
239
+
240
+ if result.backup_restored:
241
+ self.console.print(
242
+ "[yellow]⚠️ Files were automatically restored from backup[/yellow]"
243
+ )
244
+
245
+ if result.backup_metadata:
246
+ self.console.print(
247
+ f"[blue]📦 Backup available at: {result.backup_metadata.backup_directory}[/blue]"
248
+ )
249
+
192
250
  def _report_cleaning_results(self, results: list[CleaningResult]) -> None:
193
- """Report cleaning results to user."""
194
251
  successful = sum(1 for r in results if r.success)
195
252
  failed = len(results) - successful
196
253
 
197
254
  if successful > 0:
198
255
  self.console.print(
199
- f"[green]✅ Successfully cleaned {successful} files[/green]",
256
+ f"[green]✅ Successfully cleaned {successful} files[/ green]",
200
257
  )
201
258
  if failed > 0:
202
- self.console.print(f"[red]❌ Failed to clean {failed} files[/red]")
259
+ self.console.print(f"[red]❌ Failed to clean {failed} files[/ red]")
203
260
 
204
261
  def _handle_cleaning_error(self, error: Exception) -> t.NoReturn:
205
- """Handle code cleaning errors."""
206
262
  self.logger.error(f"Code cleaning failed: {error}")
207
263
  raise CrackerjackError(
208
264
  message=f"Code cleaning failed: {error}",
@@ -263,7 +319,7 @@ class CrackerjackAPI:
263
319
  ) -> PublishResult:
264
320
  try:
265
321
  self.logger.info(
266
- f"Publishing package (version_bump = {version_bump}, dry_run = {dry_run})",
322
+ f"Publishing package (version_bump={version_bump}, dry_run={dry_run})",
267
323
  )
268
324
 
269
325
  options = self._create_options(
@@ -304,7 +360,7 @@ class CrackerjackAPI:
304
360
  return self.interactive_cli.run_interactive_workflow(options)
305
361
  except Exception as e:
306
362
  self.logger.exception(f"Interactive workflow failed: {e}")
307
- self.console.print(f"[red]❌ Interactive workflow failed: {e}[/red]")
363
+ self.console.print(f"[red]❌ Interactive workflow failed: {e}[/ red]")
308
364
  return False
309
365
 
310
366
  def create_workflow_options(
@@ -371,7 +427,7 @@ class CrackerjackAPI:
371
427
  "has_requirements_txt": (
372
428
  self.project_path / "requirements.txt"
373
429
  ).exists(),
374
- "has_tests": any(self.project_path.rglob("test*.py")),
430
+ "has_tests": any(self.project_path.rglob("test *.py")),
375
431
  }
376
432
 
377
433
  except Exception as e:
@@ -458,15 +514,12 @@ class CrackerjackAPI:
458
514
  return "unknown"
459
515
 
460
516
  def _check_for_todos(self, target_dir: Path) -> list[tuple[Path, int, str]]:
461
- """Check for TODO comments in Python files."""
462
- import re
463
-
464
- task_pattern = re.compile(f"#.*?{'T'}{'O'}{'D'}{'O'}.*", re.IGNORECASE)
517
+ # Use SAFE_PATTERNS for TODO detection
518
+ task_pattern = SAFE_PATTERNS["todo_pattern"]
465
519
  python_files = self._get_python_files_for_todo_check(target_dir)
466
520
  return self._scan_files_for_todos(python_files, task_pattern)
467
521
 
468
522
  def _get_python_files_for_todo_check(self, target_dir: Path) -> list[Path]:
469
- """Get list of Python files to check for TODOs, excluding ignored directories."""
470
523
  python_files: list[Path] = []
471
524
  ignore_patterns = self._get_ignore_patterns()
472
525
 
@@ -477,7 +530,6 @@ class CrackerjackAPI:
477
530
  return python_files
478
531
 
479
532
  def _get_ignore_patterns(self) -> set[str]:
480
- """Get patterns for directories/files to ignore during TODO scanning."""
481
533
  return {
482
534
  "__pycache__",
483
535
  ".git",
@@ -486,10 +538,13 @@ class CrackerjackAPI:
486
538
  ".pytest_cache",
487
539
  "build",
488
540
  "dist",
541
+ "tests",
542
+ "test",
543
+ "examples",
544
+ "example",
489
545
  }
490
546
 
491
547
  def _should_skip_file(self, py_file: Path, ignore_patterns: set[str]) -> bool:
492
- """Check if a Python file should be skipped during TODO scanning."""
493
548
  if py_file.name.startswith("."):
494
549
  return True
495
550
 
@@ -500,7 +555,6 @@ class CrackerjackAPI:
500
555
  python_files: list[Path],
501
556
  todo_pattern: t.Any,
502
557
  ) -> list[tuple[Path, int, str]]:
503
- """Scan Python files for TODO comments."""
504
558
  todos_found: list[tuple[Path, int, str]] = []
505
559
 
506
560
  for file_path in python_files:
@@ -514,18 +568,69 @@ class CrackerjackAPI:
514
568
  file_path: Path,
515
569
  todo_pattern: t.Any,
516
570
  ) -> list[tuple[Path, int, str]]:
517
- """Scan a single file for TODO comments."""
518
571
  todos: list[tuple[Path, int, str]] = []
519
572
  from contextlib import suppress
520
573
 
521
574
  with suppress(UnicodeDecodeError, PermissionError):
522
575
  with file_path.open() as f:
523
576
  for line_no, line in enumerate(f, 1):
524
- if todo_pattern.search(line):
577
+ # For ValidatedPattern, check if applying it changes the line
578
+ # If it doesn't change, then it didn't match (identity replacement for match-only patterns)
579
+ original = line.strip()
580
+ processed = todo_pattern.apply(original)
581
+ # For TODO pattern with identity replacement, a match means no change
582
+ # But we need to check if it actually contains TODO
583
+ if "todo" in original.lower() and original == processed:
525
584
  todos.append((file_path, line_no, line))
526
585
 
527
586
  return todos
528
587
 
588
+ def _get_package_root(self) -> Path:
589
+ package_name = self._read_package_name_from_pyproject()
590
+ if package_name:
591
+ package_dir = self._find_package_directory_by_name(package_name)
592
+ if package_dir:
593
+ return package_dir
594
+
595
+ fallback_dir = self._find_fallback_package_directory()
596
+ return fallback_dir or self.project_path
597
+
598
+ def _read_package_name_from_pyproject(self) -> str | None:
599
+ pyproject_path = self.project_path / "pyproject.toml"
600
+ if not pyproject_path.exists():
601
+ return None
602
+
603
+ from contextlib import suppress
604
+
605
+ with suppress(Exception):
606
+ import tomllib
607
+
608
+ with pyproject_path.open("rb") as f:
609
+ data = tomllib.load(f)
610
+
611
+ if "project" in data and "name" in data["project"]:
612
+ return data["project"]["name"]
613
+
614
+ return None
615
+
616
+ def _find_package_directory_by_name(self, package_name: str) -> Path | None:
617
+ package_dir = self.project_path / package_name
618
+ if package_dir.exists() and package_dir.is_dir():
619
+ return package_dir
620
+ return None
621
+
622
+ def _find_fallback_package_directory(self) -> Path | None:
623
+ for possible_name in ("src", self.project_path.name):
624
+ package_dir = self.project_path / possible_name
625
+ if self._is_valid_python_package_directory(package_dir):
626
+ return package_dir
627
+ return None
628
+
629
+ def _is_valid_python_package_directory(self, directory: Path) -> bool:
630
+ if not (directory.exists() and directory.is_dir()):
631
+ return False
632
+ return any(directory.glob("*.py"))
633
+
529
634
 
530
635
  def run_quality_checks(
531
636
  project_path: Path | None = None,
@@ -541,8 +646,11 @@ def run_quality_checks(
541
646
  def clean_code(
542
647
  project_path: Path | None = None,
543
648
  backup: bool = True,
544
- ) -> list[CleaningResult]:
545
- return CrackerjackAPI(project_path=project_path).clean_code(backup=backup)
649
+ safe_mode: bool = True,
650
+ ) -> list[CleaningResult] | PackageCleaningResult:
651
+ return CrackerjackAPI(project_path=project_path).clean_code(
652
+ backup=backup, safe_mode=safe_mode
653
+ )
546
654
 
547
655
 
548
656
  def run_tests(project_path: Path | None = None, coverage: bool = False) -> TestResult: