crackerjack 0.32.0__py3-none-any.whl → 0.33.1__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 (200) hide show
  1. crackerjack/__main__.py +1350 -34
  2. crackerjack/adapters/__init__.py +17 -0
  3. crackerjack/adapters/lsp_client.py +358 -0
  4. crackerjack/adapters/rust_tool_adapter.py +194 -0
  5. crackerjack/adapters/rust_tool_manager.py +193 -0
  6. crackerjack/adapters/skylos_adapter.py +231 -0
  7. crackerjack/adapters/zuban_adapter.py +560 -0
  8. crackerjack/agents/base.py +7 -3
  9. crackerjack/agents/coordinator.py +271 -33
  10. crackerjack/agents/documentation_agent.py +9 -15
  11. crackerjack/agents/dry_agent.py +3 -15
  12. crackerjack/agents/formatting_agent.py +1 -1
  13. crackerjack/agents/import_optimization_agent.py +36 -180
  14. crackerjack/agents/performance_agent.py +17 -98
  15. crackerjack/agents/performance_helpers.py +7 -31
  16. crackerjack/agents/proactive_agent.py +1 -3
  17. crackerjack/agents/refactoring_agent.py +16 -85
  18. crackerjack/agents/refactoring_helpers.py +7 -42
  19. crackerjack/agents/security_agent.py +9 -48
  20. crackerjack/agents/test_creation_agent.py +356 -513
  21. crackerjack/agents/test_specialist_agent.py +0 -4
  22. crackerjack/api.py +6 -25
  23. crackerjack/cli/cache_handlers.py +204 -0
  24. crackerjack/cli/cache_handlers_enhanced.py +683 -0
  25. crackerjack/cli/facade.py +100 -0
  26. crackerjack/cli/handlers.py +224 -9
  27. crackerjack/cli/interactive.py +6 -4
  28. crackerjack/cli/options.py +642 -55
  29. crackerjack/cli/utils.py +2 -1
  30. crackerjack/code_cleaner.py +58 -117
  31. crackerjack/config/global_lock_config.py +8 -48
  32. crackerjack/config/hooks.py +53 -62
  33. crackerjack/core/async_workflow_orchestrator.py +24 -34
  34. crackerjack/core/autofix_coordinator.py +3 -17
  35. crackerjack/core/enhanced_container.py +64 -6
  36. crackerjack/core/file_lifecycle.py +12 -89
  37. crackerjack/core/performance.py +2 -2
  38. crackerjack/core/performance_monitor.py +15 -55
  39. crackerjack/core/phase_coordinator.py +257 -218
  40. crackerjack/core/resource_manager.py +14 -90
  41. crackerjack/core/service_watchdog.py +62 -95
  42. crackerjack/core/session_coordinator.py +149 -0
  43. crackerjack/core/timeout_manager.py +14 -72
  44. crackerjack/core/websocket_lifecycle.py +13 -78
  45. crackerjack/core/workflow_orchestrator.py +558 -240
  46. crackerjack/docs/INDEX.md +11 -0
  47. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  48. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  49. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  50. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  51. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  52. crackerjack/documentation/__init__.py +31 -0
  53. crackerjack/documentation/ai_templates.py +756 -0
  54. crackerjack/documentation/dual_output_generator.py +765 -0
  55. crackerjack/documentation/mkdocs_integration.py +518 -0
  56. crackerjack/documentation/reference_generator.py +977 -0
  57. crackerjack/dynamic_config.py +55 -50
  58. crackerjack/executors/async_hook_executor.py +10 -15
  59. crackerjack/executors/cached_hook_executor.py +117 -43
  60. crackerjack/executors/hook_executor.py +8 -34
  61. crackerjack/executors/hook_lock_manager.py +26 -183
  62. crackerjack/executors/individual_hook_executor.py +13 -11
  63. crackerjack/executors/lsp_aware_hook_executor.py +270 -0
  64. crackerjack/executors/tool_proxy.py +417 -0
  65. crackerjack/hooks/lsp_hook.py +79 -0
  66. crackerjack/intelligence/adaptive_learning.py +25 -10
  67. crackerjack/intelligence/agent_orchestrator.py +2 -5
  68. crackerjack/intelligence/agent_registry.py +34 -24
  69. crackerjack/intelligence/agent_selector.py +5 -7
  70. crackerjack/interactive.py +17 -6
  71. crackerjack/managers/async_hook_manager.py +0 -1
  72. crackerjack/managers/hook_manager.py +79 -1
  73. crackerjack/managers/publish_manager.py +66 -13
  74. crackerjack/managers/test_command_builder.py +5 -17
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +109 -7
  77. crackerjack/managers/test_manager_backup.py +10 -9
  78. crackerjack/mcp/cache.py +2 -2
  79. crackerjack/mcp/client_runner.py +1 -1
  80. crackerjack/mcp/context.py +191 -68
  81. crackerjack/mcp/dashboard.py +7 -5
  82. crackerjack/mcp/enhanced_progress_monitor.py +31 -28
  83. crackerjack/mcp/file_monitor.py +30 -23
  84. crackerjack/mcp/progress_components.py +31 -21
  85. crackerjack/mcp/progress_monitor.py +50 -53
  86. crackerjack/mcp/rate_limiter.py +6 -6
  87. crackerjack/mcp/server_core.py +161 -32
  88. crackerjack/mcp/service_watchdog.py +2 -1
  89. crackerjack/mcp/state.py +4 -7
  90. crackerjack/mcp/task_manager.py +11 -9
  91. crackerjack/mcp/tools/core_tools.py +174 -33
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +15 -12
  94. crackerjack/mcp/tools/execution_tools_backup.py +42 -30
  95. crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
  96. crackerjack/mcp/tools/intelligence_tools.py +5 -2
  97. crackerjack/mcp/tools/monitoring_tools.py +33 -70
  98. crackerjack/mcp/tools/proactive_tools.py +24 -11
  99. crackerjack/mcp/tools/progress_tools.py +5 -8
  100. crackerjack/mcp/tools/utility_tools.py +20 -14
  101. crackerjack/mcp/tools/workflow_executor.py +62 -40
  102. crackerjack/mcp/websocket/app.py +8 -0
  103. crackerjack/mcp/websocket/endpoints.py +352 -357
  104. crackerjack/mcp/websocket/jobs.py +40 -57
  105. crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
  106. crackerjack/mcp/websocket/server.py +7 -25
  107. crackerjack/mcp/websocket/websocket_handler.py +6 -17
  108. crackerjack/mixins/__init__.py +3 -0
  109. crackerjack/mixins/error_handling.py +145 -0
  110. crackerjack/models/config.py +21 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +176 -107
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/models/task.py +3 -0
  115. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  116. crackerjack/monitoring/metrics_collector.py +426 -0
  117. crackerjack/monitoring/regression_prevention.py +8 -8
  118. crackerjack/monitoring/websocket_server.py +643 -0
  119. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  120. crackerjack/orchestration/coverage_improvement.py +3 -3
  121. crackerjack/orchestration/execution_strategies.py +26 -6
  122. crackerjack/orchestration/test_progress_streamer.py +8 -5
  123. crackerjack/plugins/base.py +2 -2
  124. crackerjack/plugins/hooks.py +7 -0
  125. crackerjack/plugins/managers.py +11 -8
  126. crackerjack/security/__init__.py +0 -1
  127. crackerjack/security/audit.py +90 -105
  128. crackerjack/services/anomaly_detector.py +392 -0
  129. crackerjack/services/api_extractor.py +615 -0
  130. crackerjack/services/backup_service.py +2 -2
  131. crackerjack/services/bounded_status_operations.py +15 -152
  132. crackerjack/services/cache.py +127 -1
  133. crackerjack/services/changelog_automation.py +395 -0
  134. crackerjack/services/config.py +18 -11
  135. crackerjack/services/config_merge.py +30 -85
  136. crackerjack/services/config_template.py +506 -0
  137. crackerjack/services/contextual_ai_assistant.py +48 -22
  138. crackerjack/services/coverage_badge_service.py +171 -0
  139. crackerjack/services/coverage_ratchet.py +41 -17
  140. crackerjack/services/debug.py +3 -3
  141. crackerjack/services/dependency_analyzer.py +460 -0
  142. crackerjack/services/dependency_monitor.py +14 -11
  143. crackerjack/services/documentation_generator.py +491 -0
  144. crackerjack/services/documentation_service.py +675 -0
  145. crackerjack/services/enhanced_filesystem.py +6 -5
  146. crackerjack/services/enterprise_optimizer.py +865 -0
  147. crackerjack/services/error_pattern_analyzer.py +676 -0
  148. crackerjack/services/file_hasher.py +1 -1
  149. crackerjack/services/git.py +41 -45
  150. crackerjack/services/health_metrics.py +10 -8
  151. crackerjack/services/heatmap_generator.py +735 -0
  152. crackerjack/services/initialization.py +30 -33
  153. crackerjack/services/input_validator.py +5 -97
  154. crackerjack/services/intelligent_commit.py +327 -0
  155. crackerjack/services/log_manager.py +15 -12
  156. crackerjack/services/logging.py +4 -3
  157. crackerjack/services/lsp_client.py +628 -0
  158. crackerjack/services/memory_optimizer.py +409 -0
  159. crackerjack/services/metrics.py +42 -33
  160. crackerjack/services/parallel_executor.py +416 -0
  161. crackerjack/services/pattern_cache.py +1 -1
  162. crackerjack/services/pattern_detector.py +6 -6
  163. crackerjack/services/performance_benchmarks.py +250 -576
  164. crackerjack/services/performance_cache.py +382 -0
  165. crackerjack/services/performance_monitor.py +565 -0
  166. crackerjack/services/predictive_analytics.py +510 -0
  167. crackerjack/services/quality_baseline.py +234 -0
  168. crackerjack/services/quality_baseline_enhanced.py +646 -0
  169. crackerjack/services/quality_intelligence.py +785 -0
  170. crackerjack/services/regex_patterns.py +605 -524
  171. crackerjack/services/regex_utils.py +43 -123
  172. crackerjack/services/secure_path_utils.py +5 -164
  173. crackerjack/services/secure_status_formatter.py +30 -141
  174. crackerjack/services/secure_subprocess.py +11 -92
  175. crackerjack/services/security.py +61 -30
  176. crackerjack/services/security_logger.py +18 -22
  177. crackerjack/services/server_manager.py +124 -16
  178. crackerjack/services/status_authentication.py +16 -159
  179. crackerjack/services/status_security_manager.py +4 -131
  180. crackerjack/services/terminal_utils.py +0 -0
  181. crackerjack/services/thread_safe_status_collector.py +19 -125
  182. crackerjack/services/unified_config.py +21 -13
  183. crackerjack/services/validation_rate_limiter.py +5 -54
  184. crackerjack/services/version_analyzer.py +459 -0
  185. crackerjack/services/version_checker.py +1 -1
  186. crackerjack/services/websocket_resource_limiter.py +10 -144
  187. crackerjack/services/zuban_lsp_service.py +390 -0
  188. crackerjack/slash_commands/__init__.py +2 -7
  189. crackerjack/slash_commands/run.md +2 -2
  190. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  191. crackerjack/tools/validate_regex_patterns.py +19 -48
  192. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
  193. crackerjack-0.33.1.dist-info/RECORD +229 -0
  194. crackerjack/CLAUDE.md +0 -207
  195. crackerjack/RULES.md +0 -380
  196. crackerjack/py313.py +0 -234
  197. crackerjack-0.32.0.dist-info/RECORD +0 -180
  198. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  199. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  200. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
1
1
  import subprocess
2
+ import typing as t
2
3
  from pathlib import Path
3
4
 
4
5
  from rich.console import Console
@@ -6,10 +7,23 @@ from rich.console import Console
6
7
  from .secure_subprocess import execute_secure_subprocess
7
8
  from .security_logger import get_security_logger
8
9
 
10
+ GIT_COMMANDS = {
11
+ "git_dir": ["rev-parse", "--git-dir"],
12
+ "staged_files": ["diff", "--cached", "--name-only", "--diff-filter=ACMRT"],
13
+ "unstaged_files": ["diff", "--name-only", "--diff-filter=ACMRT"],
14
+ "untracked_files": ["ls-files", "--others", "--exclude-standard"],
15
+ "staged_files_simple": ["diff", "--cached", "--name-only"],
16
+ "add_file": ["add"],
17
+ "add_all": ["add", "-A", "."],
18
+ "commit": ["commit", "-m"],
19
+ "add_updated": ["add", "-u"],
20
+ "push_porcelain": ["push", "--porcelain"],
21
+ "current_branch": ["branch", "--show-current"],
22
+ "commits_ahead": ["rev-list[t.Any]", "--count", "@{u}..HEAD"],
23
+ }
9
24
 
10
- class FailedGitResult:
11
- """A Git result object compatible with subprocess.CompletedProcess."""
12
25
 
26
+ class FailedGitResult:
13
27
  def __init__(self, command: list[str], error: str) -> None:
14
28
  self.args = command
15
29
  self.returncode = -1
@@ -25,7 +39,6 @@ class GitService:
25
39
  def _run_git_command(
26
40
  self, args: list[str]
27
41
  ) -> subprocess.CompletedProcess[str] | FailedGitResult:
28
- """Execute Git commands with secure subprocess validation."""
29
42
  cmd = ["git", *args]
30
43
 
31
44
  try:
@@ -35,10 +48,9 @@ class GitService:
35
48
  capture_output=True,
36
49
  text=True,
37
50
  timeout=60,
38
- check=False, # Don't raise on non-zero exit codes
51
+ check=False,
39
52
  )
40
53
  except Exception as e:
41
- # Log security issues but return a compatible result
42
54
  security_logger = get_security_logger()
43
55
  security_logger.log_subprocess_failure(
44
56
  command=cmd,
@@ -46,46 +58,39 @@ class GitService:
46
58
  error_output=str(e),
47
59
  )
48
60
 
49
- # Create compatible result for Git operations
50
61
  return FailedGitResult(cmd, str(e))
51
62
 
52
63
  def is_git_repo(self) -> bool:
53
64
  try:
54
- result = self._run_git_command(["rev-parse", "- - git-dir"])
65
+ result = self._run_git_command(GIT_COMMANDS["git_dir"])
55
66
  return result.returncode == 0
56
67
  except (subprocess.SubprocessError, OSError, FileNotFoundError):
57
68
  return False
58
69
 
59
70
  def get_changed_files(self) -> list[str]:
60
71
  try:
61
- staged_result = self._run_git_command(
62
- ["diff", "--cached", "- - name-only", "- - diff-filter=ACMRT"]
63
- )
72
+ staged_result = self._run_git_command(GIT_COMMANDS["staged_files"])
64
73
  staged_files = (
65
74
  staged_result.stdout.strip().split("\n")
66
75
  if staged_result.stdout.strip()
67
76
  else []
68
77
  )
69
78
 
70
- unstaged_result = self._run_git_command(
71
- ["diff", "- - name-only", "- - diff-filter=ACMRT"]
72
- )
79
+ unstaged_result = self._run_git_command(GIT_COMMANDS["unstaged_files"])
73
80
  unstaged_files = (
74
81
  unstaged_result.stdout.strip().split("\n")
75
82
  if unstaged_result.stdout.strip()
76
83
  else []
77
84
  )
78
85
 
79
- untracked_result = self._run_git_command(
80
- ["ls-files", "--others", "- - exclude-standard"],
81
- )
86
+ untracked_result = self._run_git_command(GIT_COMMANDS["untracked_files"])
82
87
  untracked_files = (
83
88
  untracked_result.stdout.strip().split("\n")
84
89
  if untracked_result.stdout.strip()
85
90
  else []
86
91
  )
87
92
 
88
- all_files = set(staged_files + unstaged_files + untracked_files)
93
+ all_files = set[t.Any](staged_files + unstaged_files + untracked_files)
89
94
  return [f for f in all_files if f]
90
95
  except Exception as e:
91
96
  self.console.print(f"[yellow]⚠️[/ yellow] Error getting changed files: {e}")
@@ -93,7 +98,7 @@ class GitService:
93
98
 
94
99
  def get_staged_files(self) -> list[str]:
95
100
  try:
96
- result = self._run_git_command(["diff", "--cached", "- - name-only"])
101
+ result = self._run_git_command(GIT_COMMANDS["staged_files_simple"])
97
102
  return result.stdout.strip().split("\n") if result.stdout.strip() else []
98
103
  except Exception as e:
99
104
  self.console.print(f"[yellow]⚠️[/ yellow] Error getting staged files: {e}")
@@ -102,7 +107,8 @@ class GitService:
102
107
  def add_files(self, files: list[str]) -> bool:
103
108
  try:
104
109
  for file in files:
105
- result = self._run_git_command(["add", file])
110
+ cmd = GIT_COMMANDS["add_file"] + [file]
111
+ result = self._run_git_command(cmd)
106
112
  if result.returncode != 0:
107
113
  self.console.print(
108
114
  f"[red]❌[/ red] Failed to add {file}: {result.stderr}",
@@ -114,9 +120,8 @@ class GitService:
114
120
  return False
115
121
 
116
122
  def add_all_files(self) -> bool:
117
- """Stage all changes including new, modified, and deleted files."""
118
123
  try:
119
- result = self._run_git_command(["add", "-A", "."])
124
+ result = self._run_git_command(GIT_COMMANDS["add_all"])
120
125
  if result.returncode == 0:
121
126
  self.console.print("[green]✅[/ green] Staged all changes")
122
127
  return True
@@ -130,7 +135,8 @@ class GitService:
130
135
 
131
136
  def commit(self, message: str) -> bool:
132
137
  try:
133
- result = self._run_git_command(["commit", "- m", message])
138
+ cmd = GIT_COMMANDS["commit"] + [message]
139
+ result = self._run_git_command(cmd)
134
140
  if result.returncode == 0:
135
141
  self.console.print(f"[green]✅[/ green] Committed: {message}")
136
142
  return True
@@ -153,14 +159,15 @@ class GitService:
153
159
  "[yellow]🔄[/ yellow] Pre - commit hooks modified files - attempting to re-stage and retry commit"
154
160
  )
155
161
 
156
- add_result = self._run_git_command(["add", "- u"])
162
+ add_result = self._run_git_command(GIT_COMMANDS["add_updated"])
157
163
  if add_result.returncode != 0:
158
164
  self.console.print(
159
165
  f"[red]❌[/ red] Failed to re-stage files: {add_result.stderr}"
160
166
  )
161
167
  return False
162
168
 
163
- retry_result = self._run_git_command(["commit", "- m", message])
169
+ cmd = GIT_COMMANDS["commit"] + [message]
170
+ retry_result = self._run_git_command(cmd)
164
171
  if retry_result.returncode == 0:
165
172
  self.console.print(
166
173
  f"[green]✅[/ green] Committed after re-staging: {message}"
@@ -187,8 +194,7 @@ class GitService:
187
194
 
188
195
  def push(self) -> bool:
189
196
  try:
190
- # Get detailed push information
191
- result = self._run_git_command(["push", "--porcelain"])
197
+ result = self._run_git_command(GIT_COMMANDS["push_porcelain"])
192
198
  if result.returncode == 0:
193
199
  self._display_push_success(result.stdout)
194
200
  return True
@@ -199,7 +205,6 @@ class GitService:
199
205
  return False
200
206
 
201
207
  def _display_push_success(self, push_output: str) -> None:
202
- """Display detailed push success information."""
203
208
  lines = push_output.strip().split("\n") if push_output.strip() else []
204
209
 
205
210
  if not lines:
@@ -210,15 +215,12 @@ class GitService:
210
215
  self._display_push_results(pushed_refs)
211
216
 
212
217
  def _display_no_commits_message(self) -> None:
213
- """Display message for no new commits."""
214
218
  self.console.print("[green]✅[/ green] Pushed to remote (no new commits)")
215
219
 
216
220
  def _parse_pushed_refs(self, lines: list[str]) -> list[str]:
217
- """Parse pushed references from git output."""
218
221
  pushed_refs = []
219
222
  for line in lines:
220
223
  if line.startswith(("*", "+", "=")):
221
- # Parse porcelain output: flag:from:to summary
222
224
  parts = line.split("\t")
223
225
  if len(parts) >= 2:
224
226
  summary = parts[1] if len(parts) > 1 else ""
@@ -226,22 +228,18 @@ class GitService:
226
228
  return pushed_refs
227
229
 
228
230
  def _display_push_results(self, pushed_refs: list[str]) -> None:
229
- """Display the push results to console."""
230
231
  if pushed_refs:
231
232
  self.console.print(
232
- f"[green]✅[/ green] Successfully pushed {len(pushed_refs)} ref(s) to remote:"
233
+ f"[green]✅[/ green] Successfully pushed {len(pushed_refs)} ref(s) to remote: "
233
234
  )
234
235
  for ref in pushed_refs:
235
- self.console.print(f" [dim]→ {ref}[/ dim]")
236
+ self.console.print(f" [dim]→ {ref}[/ dim]")
236
237
  else:
237
- # Get commit count as fallback
238
238
  self._display_commit_count_push()
239
239
 
240
240
  def _display_commit_count_push(self) -> None:
241
- """Fallback method to show commit count information."""
242
241
  try:
243
- # Get commits ahead of remote
244
- result = self._run_git_command(["rev-list", "--count", "@{u}..HEAD"])
242
+ result = self._run_git_command(GIT_COMMANDS["commits_ahead"])
245
243
  if result.returncode == 0 and result.stdout.strip().isdigit():
246
244
  commit_count = int(result.stdout.strip())
247
245
  if commit_count > 0:
@@ -253,24 +251,23 @@ class GitService:
253
251
  "[green]✅[/ green] Pushed to remote (up to date)"
254
252
  )
255
253
  else:
256
- # Even more basic fallback
257
254
  self.console.print("[green]✅[/ green] Successfully pushed to remote")
258
255
  except (ValueError, Exception):
259
256
  self.console.print("[green]✅[/ green] Successfully pushed to remote")
260
257
 
261
258
  def get_current_branch(self) -> str | None:
262
259
  try:
263
- result = self._run_git_command(["branch", "- - show-current"])
260
+ result = self._run_git_command(GIT_COMMANDS["current_branch"])
264
261
  return result.stdout.strip() if result.returncode == 0 else None
265
262
  except (subprocess.SubprocessError, OSError, FileNotFoundError):
266
263
  return None
267
264
 
268
- def get_commit_message_suggestions(self, files: list[str]) -> list[str]:
269
- if not files:
265
+ def get_commit_message_suggestions(self, changed_files: list[str]) -> list[str]:
266
+ if not changed_files:
270
267
  return ["Update project files"]
271
- file_categories = self._categorize_files(files)
268
+ file_categories = self._categorize_files(changed_files)
272
269
  messages = self._generate_category_messages(file_categories)
273
- messages.extend(self._generate_specific_messages(files))
270
+ messages.extend(self._generate_specific_messages(changed_files))
274
271
 
275
272
  return messages[:5]
276
273
 
@@ -322,11 +319,10 @@ class GitService:
322
319
  return messages
323
320
 
324
321
  def get_unpushed_commit_count(self) -> int:
325
- """Get the number of unpushed commits."""
326
322
  from contextlib import suppress
327
323
 
328
324
  with suppress(ValueError, Exception):
329
- result = self._run_git_command(["rev-list", "--count", "@{u}..HEAD"])
325
+ result = self._run_git_command(GIT_COMMANDS["commits_ahead"])
330
326
  if result.returncode == 0 and result.stdout.strip().isdigit():
331
327
  return int(result.stdout.strip())
332
328
  return 0
@@ -9,6 +9,7 @@ from datetime import datetime
9
9
  from pathlib import Path
10
10
  from typing import Any
11
11
 
12
+ import requests
12
13
  from rich.console import Console
13
14
 
14
15
  from crackerjack.models.protocols import FileSystemInterface
@@ -18,7 +19,7 @@ from crackerjack.models.protocols import FileSystemInterface
18
19
  class ProjectHealth:
19
20
  lint_error_trend: list[int] = field(default_factory=list)
20
21
  test_coverage_trend: list[float] = field(default_factory=list)
21
- dependency_age: dict[str, int] = field(default_factory=dict)
22
+ dependency_age: dict[str, int] = field(default_factory=dict[str, t.Any])
22
23
  config_completeness: float = 0.0
23
24
  last_updated: float = field(default_factory=time.time)
24
25
 
@@ -344,14 +345,13 @@ class HealthMetricsService:
344
345
 
345
346
  def _fetch_package_data(self, package_name: str) -> dict[str, t.Any] | None:
346
347
  try:
347
- import urllib.request
348
348
  from urllib.parse import urlparse
349
349
 
350
- url = f"https://pypi.org/pypi/{package_name}/json"
350
+ url = f"https: //pypi.org/pypi/{package_name}/json"
351
351
 
352
352
  parsed = urlparse(url)
353
353
  if parsed.scheme != "https" or parsed.netloc != "pypi.org":
354
- msg = f"Invalid URL: only https://pypi.org URLs are allowed, got {url}"
354
+ msg = f"Invalid URL: only https: //pypi.org URLs are allowed, got {url}"
355
355
  raise ValueError(msg)
356
356
 
357
357
  if not parsed.path.startswith("/pypi/") or not parsed.path.endswith(
@@ -360,9 +360,10 @@ class HealthMetricsService:
360
360
  msg = f"Invalid PyPI API path: {parsed.path}"
361
361
  raise ValueError(msg)
362
362
 
363
- # B310: Safe urllib.urlopen with scheme validation above
364
- with urllib.request.urlopen(url, timeout=10) as response: # nosec B310
365
- return json.load(response)
363
+ response = requests.get(url, timeout=10, verify=True)
364
+ response.raise_for_status()
365
+ json_result = response.json()
366
+ return t.cast(dict[str, t.Any] | None, json_result)
366
367
  except Exception:
367
368
  return None
368
369
 
@@ -378,7 +379,8 @@ class HealthMetricsService:
378
379
  if not release_info:
379
380
  return None
380
381
 
381
- return release_info[0].get("upload_time", "")
382
+ upload_time_raw = release_info[0].get("upload_time", "")
383
+ return t.cast(str | None, upload_time_raw)
382
384
 
383
385
  def _calculate_days_since_upload(self, upload_time: str) -> int | None:
384
386
  try: