crackerjack 0.31.10__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 +281 -94
  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 +343 -209
  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 +17 -63
  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 +44 -73
  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 +71 -47
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +276 -428
  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.10.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.10.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
@@ -3,35 +3,63 @@ from pathlib import Path
3
3
 
4
4
  from rich.console import Console
5
5
 
6
+ from .secure_subprocess import execute_secure_subprocess
7
+ from .security_logger import get_security_logger
8
+
9
+
10
+ class FailedGitResult:
11
+ """A Git result object compatible with subprocess.CompletedProcess."""
12
+
13
+ def __init__(self, command: list[str], error: str) -> None:
14
+ self.args = command
15
+ self.returncode = -1
16
+ self.stdout = ""
17
+ self.stderr = f"Git security validation failed: {error}"
18
+
6
19
 
7
20
  class GitService:
8
21
  def __init__(self, console: Console, pkg_path: Path | None = None) -> None:
9
22
  self.console = console
10
23
  self.pkg_path = pkg_path or Path.cwd()
11
24
 
12
- def _run_git_command(self, args: list[str]) -> subprocess.CompletedProcess[str]:
25
+ def _run_git_command(
26
+ self, args: list[str]
27
+ ) -> subprocess.CompletedProcess[str] | FailedGitResult:
28
+ """Execute Git commands with secure subprocess validation."""
13
29
  cmd = ["git", *args]
14
- return subprocess.run(
15
- cmd,
16
- check=False,
17
- cwd=self.pkg_path,
18
- capture_output=True,
19
- text=True,
20
- timeout=60,
21
- )
30
+
31
+ try:
32
+ return execute_secure_subprocess(
33
+ command=cmd,
34
+ cwd=self.pkg_path,
35
+ capture_output=True,
36
+ text=True,
37
+ timeout=60,
38
+ check=False, # Don't raise on non-zero exit codes
39
+ )
40
+ except Exception as e:
41
+ # Log security issues but return a compatible result
42
+ security_logger = get_security_logger()
43
+ security_logger.log_subprocess_failure(
44
+ command=cmd,
45
+ exit_code=-1,
46
+ error_output=str(e),
47
+ )
48
+
49
+ # Create compatible result for Git operations
50
+ return FailedGitResult(cmd, str(e))
22
51
 
23
52
  def is_git_repo(self) -> bool:
24
53
  try:
25
- result = self._run_git_command(["rev-parse", "--git-dir"])
54
+ result = self._run_git_command(["rev-parse", "- - git-dir"])
26
55
  return result.returncode == 0
27
56
  except (subprocess.SubprocessError, OSError, FileNotFoundError):
28
57
  return False
29
58
 
30
59
  def get_changed_files(self) -> list[str]:
31
60
  try:
32
- # Get staged files excluding deletions
33
61
  staged_result = self._run_git_command(
34
- ["diff", "--cached", "--name-only", "--diff-filter=ACMRT"]
62
+ ["diff", "--cached", "- - name-only", "- - diff-filter=ACMRT"]
35
63
  )
36
64
  staged_files = (
37
65
  staged_result.stdout.strip().split("\n")
@@ -39,9 +67,8 @@ class GitService:
39
67
  else []
40
68
  )
41
69
 
42
- # Get unstaged files excluding deletions
43
70
  unstaged_result = self._run_git_command(
44
- ["diff", "--name-only", "--diff-filter=ACMRT"]
71
+ ["diff", "- - name-only", "- - diff-filter=ACMRT"]
45
72
  )
46
73
  unstaged_files = (
47
74
  unstaged_result.stdout.strip().split("\n")
@@ -49,9 +76,8 @@ class GitService:
49
76
  else []
50
77
  )
51
78
 
52
- # Get untracked files
53
79
  untracked_result = self._run_git_command(
54
- ["ls-files", "--others", "--exclude-standard"],
80
+ ["ls-files", "--others", "- - exclude-standard"],
55
81
  )
56
82
  untracked_files = (
57
83
  untracked_result.stdout.strip().split("\n")
@@ -62,15 +88,15 @@ class GitService:
62
88
  all_files = set(staged_files + unstaged_files + untracked_files)
63
89
  return [f for f in all_files if f]
64
90
  except Exception as e:
65
- self.console.print(f"[yellow]⚠️[/yellow] Error getting changed files: {e}")
91
+ self.console.print(f"[yellow]⚠️[/ yellow] Error getting changed files: {e}")
66
92
  return []
67
93
 
68
94
  def get_staged_files(self) -> list[str]:
69
95
  try:
70
- result = self._run_git_command(["diff", "--cached", "--name-only"])
96
+ result = self._run_git_command(["diff", "--cached", "- - name-only"])
71
97
  return result.stdout.strip().split("\n") if result.stdout.strip() else []
72
98
  except Exception as e:
73
- self.console.print(f"[yellow]⚠️[/yellow] Error getting staged files: {e}")
99
+ self.console.print(f"[yellow]⚠️[/ yellow] Error getting staged files: {e}")
74
100
  return []
75
101
 
76
102
  def add_files(self, files: list[str]) -> bool:
@@ -79,30 +105,29 @@ class GitService:
79
105
  result = self._run_git_command(["add", file])
80
106
  if result.returncode != 0:
81
107
  self.console.print(
82
- f"[red]❌[/red] Failed to add {file}: {result.stderr}",
108
+ f"[red]❌[/ red] Failed to add {file}: {result.stderr}",
83
109
  )
84
110
  return False
85
111
  return True
86
112
  except Exception as e:
87
- self.console.print(f"[red]❌[/red] Error adding files: {e}")
113
+ self.console.print(f"[red]❌[/ red] Error adding files: {e}")
88
114
  return False
89
115
 
90
116
  def commit(self, message: str) -> bool:
91
117
  try:
92
- result = self._run_git_command(["commit", "-m", message])
118
+ result = self._run_git_command(["commit", "- m", message])
93
119
  if result.returncode == 0:
94
- self.console.print(f"[green]✅[/green] Committed: {message}")
120
+ self.console.print(f"[green]✅[/ green] Committed: {message}")
95
121
  return True
96
122
 
97
123
  return self._handle_commit_failure(result, message)
98
124
  except Exception as e:
99
- self.console.print(f"[red]❌[/red] Error committing: {e}")
125
+ self.console.print(f"[red]❌[/ red] Error committing: {e}")
100
126
  return False
101
127
 
102
128
  def _handle_commit_failure(
103
- self, result: subprocess.CompletedProcess[str], message: str
129
+ self, result: subprocess.CompletedProcess[str] | FailedGitResult, message: str
104
130
  ) -> bool:
105
- # Check if pre-commit hooks modified files and need re-staging
106
131
  if "files were modified by this hook" in result.stderr:
107
132
  return self._retry_commit_after_restage(message)
108
133
 
@@ -110,57 +135,56 @@ class GitService:
110
135
 
111
136
  def _retry_commit_after_restage(self, message: str) -> bool:
112
137
  self.console.print(
113
- "[yellow]🔄[/yellow] Pre-commit hooks modified files - attempting to re-stage and retry commit"
138
+ "[yellow]🔄[/ yellow] Pre - commit hooks modified files - attempting to re-stage and retry commit"
114
139
  )
115
140
 
116
- # Re-stage all modified files
117
- add_result = self._run_git_command(["add", "-u"])
141
+ add_result = self._run_git_command(["add", "- u"])
118
142
  if add_result.returncode != 0:
119
143
  self.console.print(
120
- f"[red]❌[/red] Failed to re-stage files: {add_result.stderr}"
144
+ f"[red]❌[/ red] Failed to re-stage files: {add_result.stderr}"
121
145
  )
122
146
  return False
123
147
 
124
- # Retry the commit
125
- retry_result = self._run_git_command(["commit", "-m", message])
148
+ retry_result = self._run_git_command(["commit", "- m", message])
126
149
  if retry_result.returncode == 0:
127
150
  self.console.print(
128
- f"[green]✅[/green] Committed after re-staging: {message}"
151
+ f"[green]✅[/ green] Committed after re-staging: {message}"
129
152
  )
130
153
  return True
131
154
 
132
155
  self.console.print(
133
- f"[red]❌[/red] Commit failed on retry: {retry_result.stderr}"
156
+ f"[red]❌[/ red] Commit failed on retry: {retry_result.stderr}"
134
157
  )
135
158
  return False
136
159
 
137
- def _handle_hook_error(self, result: subprocess.CompletedProcess[str]) -> bool:
138
- # When git commit fails due to pre-commit hooks, stderr contains hook output
160
+ def _handle_hook_error(
161
+ self, result: subprocess.CompletedProcess[str] | FailedGitResult
162
+ ) -> bool:
139
163
  if "pre-commit" in result.stderr or "hook" in result.stderr.lower():
140
- self.console.print("[red]❌[/red] Commit blocked by pre-commit hooks")
164
+ self.console.print("[red]❌[/ red] Commit blocked by pre-commit hooks")
141
165
  if result.stderr.strip():
142
166
  self.console.print(
143
- f"[yellow]Hook output:[/yellow]\n{result.stderr.strip()}"
167
+ f"[yellow]Hook output: [/ yellow]\n{result.stderr.strip()}"
144
168
  )
145
169
  else:
146
- self.console.print(f"[red]❌[/red] Commit failed: {result.stderr}")
170
+ self.console.print(f"[red]❌[/ red] Commit failed: {result.stderr}")
147
171
  return False
148
172
 
149
173
  def push(self) -> bool:
150
174
  try:
151
175
  result = self._run_git_command(["push"])
152
176
  if result.returncode == 0:
153
- self.console.print("[green]✅[/green] Pushed to remote")
177
+ self.console.print("[green]✅[/ green] Pushed to remote")
154
178
  return True
155
- self.console.print(f"[red]❌[/red] Push failed: {result.stderr}")
179
+ self.console.print(f"[red]❌[/ red] Push failed: {result.stderr}")
156
180
  return False
157
181
  except Exception as e:
158
- self.console.print(f"[red]❌[/red] Error pushing: {e}")
182
+ self.console.print(f"[red]❌[/ red] Error pushing: {e}")
159
183
  return False
160
184
 
161
185
  def get_current_branch(self) -> str | None:
162
186
  try:
163
- result = self._run_git_command(["branch", "--show-current"])
187
+ result = self._run_git_command(["branch", "- - show-current"])
164
188
  return result.stdout.strip() if result.returncode == 0 else None
165
189
  except (subprocess.SubprocessError, OSError, FileNotFoundError):
166
190
  return None
@@ -176,10 +200,10 @@ class GitService:
176
200
 
177
201
  def _categorize_files(self, files: list[str]) -> set[str]:
178
202
  categories = {
179
- "docs": ["README", "CLAUDE", "docs/", ".md"],
180
- "tests": ["test_", "tests/", "conftest.py"],
203
+ "docs": ["README", "CLAUDE", "docs /", ".md"],
204
+ "tests": ["test_", "tests /", "conftest.py"],
181
205
  "config": ["pyproject.toml", ".yaml", ".yml", ".json", ".gitignore"],
182
- "ci": [".github/", "ci/", ".pre-commit"],
206
+ "ci": [".github /", "ci /", ".pre-commit"],
183
207
  "deps": ["requirements", "uv.lock", "Pipfile"],
184
208
  }
185
209
  file_categories: set[str] = set()
@@ -205,7 +229,7 @@ class GitService:
205
229
  "docs": "Update documentation",
206
230
  "tests": "Update tests",
207
231
  "config": "Update configuration",
208
- "ci": "Update CI/CD configuration",
232
+ "ci": "Update CI / CD configuration",
209
233
  "deps": "Update dependencies",
210
234
  }
211
235
  return [category_messages.get(category, "Update core functionality")]
@@ -41,7 +41,7 @@ class ProjectHealth:
41
41
  return False
42
42
 
43
43
  recent = values[-min_points:]
44
- # Performance: Use pairwise comparison with zip
44
+
45
45
  return all(a <= b for a, b in zip(recent, recent[1:]))
46
46
 
47
47
  def _is_trending_down(
@@ -51,7 +51,7 @@ class ProjectHealth:
51
51
  return False
52
52
 
53
53
  recent = values[-min_points:]
54
- # Performance: Use pairwise comparison with zip
54
+
55
55
  return all(a >= b for a, b in zip(recent, recent[1:]))
56
56
 
57
57
  def get_health_score(self) -> float:
@@ -88,11 +88,11 @@ class ProjectHealth:
88
88
 
89
89
  if self._is_trending_up(self.lint_error_trend):
90
90
  recommendations.append(
91
- "🔧 Lint errors are increasing - consider running formatting tools",
91
+ "🔧 Lint errors are increasing-consider running formatting tools",
92
92
  )
93
93
 
94
94
  if self._is_trending_down(self.test_coverage_trend):
95
- recommendations.append("🧪 Test coverage is declining - add more tests")
95
+ recommendations.append("🧪 Test coverage is declining-add more tests")
96
96
 
97
97
  if any(age > 365 for age in self.dependency_age.values()):
98
98
  old_deps: list[str] = [
@@ -104,7 +104,7 @@ class ProjectHealth:
104
104
 
105
105
  if self.config_completeness < 0.5:
106
106
  recommendations.append(
107
- "⚙️ Project configuration is incomplete - run crackerjack init",
107
+ "⚙️ Project configuration is incomplete-run crackerjack init",
108
108
  )
109
109
  elif self.config_completeness < 0.8:
110
110
  recommendations.append("⚙️ Project configuration could be improved")
@@ -179,13 +179,13 @@ class HealthMetricsService:
179
179
  json.dump(data, f, indent=2)
180
180
  except Exception as e:
181
181
  self.console.print(
182
- f"[yellow]Warning: Failed to save health metrics: {e}[/yellow]",
182
+ f"[yellow]Warning: Failed to save health metrics: {e}[/ yellow]",
183
183
  )
184
184
 
185
185
  def _count_lint_errors(self) -> int | None:
186
186
  with suppress(Exception):
187
187
  result = subprocess.run(
188
- ["uv", "run", "ruff", "check", ".", "--output-format=json"],
188
+ ["uv", "run", "ruff", "check", ".", "- - output-format=json"],
189
189
  check=False,
190
190
  capture_output=True,
191
191
  text=True,
@@ -238,12 +238,12 @@ class HealthMetricsService:
238
238
  "uv",
239
239
  "run",
240
240
  "python",
241
- "-m",
241
+ "- m",
242
242
  "pytest",
243
- "--cov=.",
244
- "--cov-report=json",
243
+ "--cov =.",
244
+ "- - cov-report=json",
245
245
  "--tb=no",
246
- "-q",
246
+ "- q",
247
247
  "--maxfail=1",
248
248
  ],
249
249
  check=False,
@@ -322,7 +322,7 @@ class HealthMetricsService:
322
322
  if not dep_spec or dep_spec.startswith("-"):
323
323
  return None
324
324
 
325
- for operator in (">=", "<=", "==", "~=", "!=", ">", "<"):
325
+ for operator in ("> =", "< =", "= =", "~=", "! =", ">", "<"):
326
326
  if operator in dep_spec:
327
327
  return dep_spec.split(operator)[0].strip()
328
328
 
@@ -345,13 +345,22 @@ class HealthMetricsService:
345
345
  def _fetch_package_data(self, package_name: str) -> dict[str, t.Any] | None:
346
346
  try:
347
347
  import urllib.request
348
+ from urllib.parse import urlparse
348
349
 
349
350
  url = f"https://pypi.org/pypi/{package_name}/json"
350
351
 
351
- if not url.startswith("https://pypi.org/"):
352
- msg = f"Invalid URL scheme: {url}"
352
+ parsed = urlparse(url)
353
+ if parsed.scheme != "https" or parsed.netloc != "pypi.org":
354
+ msg = f"Invalid URL: only https://pypi.org URLs are allowed, got {url}"
355
+ raise ValueError(msg)
356
+
357
+ if not parsed.path.startswith("/pypi/") or not parsed.path.endswith(
358
+ "/json"
359
+ ):
360
+ msg = f"Invalid PyPI API path: {parsed.path}"
353
361
  raise ValueError(msg)
354
362
 
363
+ # B310: Safe urllib.urlopen with scheme validation above
355
364
  with urllib.request.urlopen(url, timeout=10) as response: # nosec B310
356
365
  return json.load(response)
357
366
  except Exception:
@@ -457,7 +466,7 @@ class HealthMetricsService:
457
466
  def _assess_precommit_config(self) -> tuple[float, int]:
458
467
  precommit_files = [
459
468
  self.project_root / ".pre-commit-config.yaml",
460
- self.project_root / ".pre-commit-config.yml",
469
+ self.project_root / ".pre - commit-config.yml",
461
470
  ]
462
471
  score = 0.1 if any(f.exists() for f in precommit_files) else 0.0
463
472
  return score, 1
@@ -489,7 +498,6 @@ class HealthMetricsService:
489
498
  return health
490
499
 
491
500
  def report_health_status(self, health: ProjectHealth) -> None:
492
- """Generate and display comprehensive project health report."""
493
501
  health_score = health.get_health_score()
494
502
 
495
503
  self._print_health_summary(health_score)
@@ -497,18 +505,16 @@ class HealthMetricsService:
497
505
  self._print_health_recommendations(health)
498
506
 
499
507
  def _print_health_summary(self, health_score: float) -> None:
500
- """Print the overall health score with appropriate styling."""
501
508
  status_icon, status_text, status_color = self._get_health_status_display(
502
509
  health_score,
503
510
  )
504
511
 
505
- self.console.print("\n[bold]📊 Project Health Report[/bold]")
512
+ self.console.print("\n[bold]📊 Project Health Report[/ bold]")
506
513
  self.console.print(
507
- f"{status_icon} Overall Health: [{status_color}]{status_text} ({health_score:.1%})[/{status_color}]",
514
+ f"{status_icon} Overall Health: [{status_color}]{status_text} ({health_score: .1 %})[/{status_color}]",
508
515
  )
509
516
 
510
517
  def _get_health_status_display(self, health_score: float) -> tuple[str, str, str]:
511
- """Get display elements (icon, text, color) for health score."""
512
518
  if health_score >= 0.8:
513
519
  return "🟢", "Excellent", "green"
514
520
  if health_score >= 0.6:
@@ -518,32 +524,30 @@ class HealthMetricsService:
518
524
  return "🔴", "Poor", "red"
519
525
 
520
526
  def _print_health_metrics(self, health: ProjectHealth) -> None:
521
- """Print detailed health metrics."""
522
527
  if health.lint_error_trend:
523
528
  recent_errors = health.lint_error_trend[-1]
524
529
  self.console.print(f"🔧 Lint Errors: {recent_errors}")
525
530
 
526
531
  if health.test_coverage_trend:
527
532
  recent_coverage = health.test_coverage_trend[-1]
528
- self.console.print(f"🧪 Test Coverage: {recent_coverage:.1f}%")
533
+ self.console.print(f"🧪 Test Coverage: {recent_coverage: .1f}%")
529
534
 
530
535
  if health.dependency_age:
531
536
  avg_age = sum(health.dependency_age.values()) / len(health.dependency_age)
532
- self.console.print(f"📦 Avg Dependency Age: {avg_age:.0f} days")
537
+ self.console.print(f"📦 Avg Dependency Age: {avg_age: .0f} days")
533
538
 
534
- self.console.print(f"⚙️ Config Completeness: {health.config_completeness:.1%}")
539
+ self.console.print(f"⚙️ Config Completeness: {health.config_completeness: .1 %}")
535
540
 
536
541
  def _print_health_recommendations(self, health: ProjectHealth) -> None:
537
- """Print health recommendations and init suggestions."""
538
542
  recommendations = health.get_recommendations()
539
543
  if recommendations:
540
- self.console.print("\n[bold]💡 Recommendations:[/bold]")
544
+ self.console.print("\n[bold]💡 Recommendations: [/ bold]")
541
545
  for rec in recommendations:
542
546
  self.console.print(f" {rec}")
543
547
 
544
548
  if health.needs_init():
545
549
  self.console.print(
546
- "\n[bold yellow]⚠️ Consider running `crackerjack --init` to improve project health[/bold yellow]",
550
+ "\n[bold yellow]⚠️ Consider running `crackerjack --init` to improve project health[/ bold yellow]",
547
551
  )
548
552
 
549
553
  def get_health_trend_summary(self, days: int = 30) -> dict[str, Any]: