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
@@ -67,7 +67,6 @@ class DependencyMonitorService:
67
67
  return False
68
68
 
69
69
  def _parse_dependencies(self) -> dict[str, str]:
70
- """Parse dependencies from pyproject.toml file."""
71
70
  try:
72
71
  with self.pyproject_path.open("rb") as f:
73
72
  data = tomllib.load(f)
@@ -82,7 +81,7 @@ class DependencyMonitorService:
82
81
 
83
82
  except Exception as e:
84
83
  self.console.print(
85
- f"[yellow]Warning: Failed to parse pyproject.toml: {e}[/yellow]",
84
+ f"[yellow]Warning: Failed to parse pyproject.toml: {e}[/ yellow]",
86
85
  )
87
86
  return {}
88
87
 
@@ -91,7 +90,6 @@ class DependencyMonitorService:
91
90
  project_data: dict[str, t.Any],
92
91
  dependencies: dict[str, str],
93
92
  ) -> None:
94
- """Extract main dependencies from project data."""
95
93
  if "dependencies" not in project_data:
96
94
  return
97
95
 
@@ -105,7 +103,6 @@ class DependencyMonitorService:
105
103
  project_data: dict[str, t.Any],
106
104
  dependencies: dict[str, str],
107
105
  ) -> None:
108
- """Extract optional dependencies from project data."""
109
106
  if "optional-dependencies" not in project_data:
110
107
  return
111
108
 
@@ -119,7 +116,7 @@ class DependencyMonitorService:
119
116
  if not spec or spec.startswith("-"):
120
117
  return None, None
121
118
 
122
- for operator in (">=", "<=", "==", "~=", "!=", ">", "<"):
119
+ for operator in ("> =", "< =", "= =", "~=", "! =", ">", "<"):
123
120
  if operator in spec:
124
121
  parts = spec.split(operator, 1)
125
122
  if len(parts) == 2:
@@ -180,7 +177,6 @@ class DependencyMonitorService:
180
177
  command_template: list[str],
181
178
  parser_func: t.Callable[[t.Any], list[DependencyVulnerability]],
182
179
  ) -> list[DependencyVulnerability]:
183
- """Common logic for running vulnerability scanning tools."""
184
180
  try:
185
181
  temp_file = self._create_requirements_file(dependencies)
186
182
  try:
@@ -197,14 +193,13 @@ class DependencyMonitorService:
197
193
  json.JSONDecodeError,
198
194
  Exception,
199
195
  ):
200
- return [] # Vulnerability check failed, return empty list
196
+ return []
201
197
 
202
198
  def _create_requirements_file(self, dependencies: dict[str, str]) -> str:
203
- """Create temporary requirements file for vulnerability scanning."""
204
199
  with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
205
200
  for package, version in dependencies.items():
206
201
  if version != "latest":
207
- f.write(f"{package}=={version}\n")
202
+ f.write(f"{package}= ={version}\n")
208
203
  else:
209
204
  f.write(f"{package}\n")
210
205
  return f.name
@@ -214,7 +209,6 @@ class DependencyMonitorService:
214
209
  command_template: list[str],
215
210
  temp_file: str,
216
211
  ) -> subprocess.CompletedProcess[str]:
217
- """Execute vulnerability scanning command with temp file."""
218
212
  cmd = [part.replace("__TEMP_FILE__", temp_file) for part in command_template]
219
213
  return subprocess.run(
220
214
  cmd,
@@ -229,7 +223,6 @@ class DependencyMonitorService:
229
223
  result: subprocess.CompletedProcess[str],
230
224
  parser_func: t.Callable[[t.Any], list[DependencyVulnerability]],
231
225
  ) -> list[DependencyVulnerability]:
232
- """Process vulnerability scan result using appropriate parser."""
233
226
  if result.returncode == 0:
234
227
  return []
235
228
 
@@ -309,10 +302,8 @@ class DependencyMonitorService:
309
302
  cache: dict[str, t.Any],
310
303
  current_time: float,
311
304
  ) -> MajorUpdate | None:
312
- """Check if a specific package has a major update available."""
313
305
  cache_key = self._build_cache_key(package, current_version)
314
306
 
315
- # Try to get from cache first
316
307
  cached_update = self._get_cached_major_update(
317
308
  cache_key,
318
309
  cache,
@@ -323,7 +314,6 @@ class DependencyMonitorService:
323
314
  if cached_update is not None:
324
315
  return cached_update
325
316
 
326
- # Check for updates and update cache
327
317
  return self._fetch_and_cache_update_info(
328
318
  package,
329
319
  current_version,
@@ -333,7 +323,6 @@ class DependencyMonitorService:
333
323
  )
334
324
 
335
325
  def _build_cache_key(self, package: str, current_version: str) -> str:
336
- """Build cache key for package version."""
337
326
  return f"{package}_{current_version}"
338
327
 
339
328
  def _get_cached_major_update(
@@ -344,7 +333,6 @@ class DependencyMonitorService:
344
333
  package: str,
345
334
  current_version: str,
346
335
  ) -> MajorUpdate | None:
347
- """Get major update from cache if available and valid."""
348
336
  if not self._is_cache_entry_valid(cache_key, cache, current_time):
349
337
  return None
350
338
 
@@ -364,13 +352,12 @@ class DependencyMonitorService:
364
352
  cache: dict[str, t.Any],
365
353
  current_time: float,
366
354
  ) -> bool:
367
- """Check if cache entry exists and is not expired."""
368
355
  if cache_key not in cache:
369
356
  return False
370
357
 
371
358
  cached_data = cache[cache_key]
372
359
  cache_age = current_time - cached_data["timestamp"]
373
- return cache_age < 86400 # Not expired (24 hours)
360
+ return cache_age < 86400
374
361
 
375
362
  def _create_major_update_from_cache(
376
363
  self,
@@ -378,7 +365,6 @@ class DependencyMonitorService:
378
365
  current_version: str,
379
366
  cached_data: dict[str, t.Any],
380
367
  ) -> MajorUpdate:
381
- """Create MajorUpdate instance from cached data."""
382
368
  return MajorUpdate(
383
369
  package=package,
384
370
  current_version=current_version,
@@ -395,7 +381,6 @@ class DependencyMonitorService:
395
381
  cache: dict[str, t.Any],
396
382
  current_time: float,
397
383
  ) -> MajorUpdate | None:
398
- """Fetch latest version info and cache the result."""
399
384
  latest_info = self._get_latest_version_info(package)
400
385
  if not latest_info:
401
386
  return None
@@ -427,7 +412,6 @@ class DependencyMonitorService:
427
412
  latest_info: dict[str, t.Any],
428
413
  has_major_update: bool,
429
414
  ) -> MajorUpdate | None:
430
- """Create MajorUpdate instance if there is a major update available."""
431
415
  if not has_major_update:
432
416
  return None
433
417
 
@@ -447,7 +431,6 @@ class DependencyMonitorService:
447
431
  has_major_update: bool,
448
432
  latest_info: dict[str, t.Any],
449
433
  ) -> None:
450
- """Update cache with latest version information."""
451
434
  cache[cache_key] = {
452
435
  "timestamp": current_time,
453
436
  "has_major_update": has_major_update,
@@ -464,23 +447,40 @@ class DependencyMonitorService:
464
447
  return None
465
448
 
466
449
  def _fetch_pypi_data(self, package: str) -> dict[str, t.Any]:
467
- """Fetch package data from PyPI API."""
468
450
  import urllib.request
451
+ from urllib.parse import urlparse
469
452
 
470
453
  url = f"https://pypi.org/pypi/{package}/json"
471
454
  self._validate_pypi_url(url)
472
455
 
456
+ parsed = urlparse(url)
457
+ # Restrict to https scheme only for security (B310)
458
+ if parsed.scheme != "https" or parsed.netloc != "pypi.org":
459
+ msg = f"Invalid URL: only https://pypi.org URLs are allowed, got {url}"
460
+ raise ValueError(msg)
461
+
462
+ # B310: Safe urllib.urlopen with scheme validation
473
463
  with urllib.request.urlopen(url, timeout=10) as response: # nosec B310
474
464
  return json.load(response)
475
465
 
476
466
  def _validate_pypi_url(self, url: str) -> None:
477
- """Validate PyPI URL for security."""
478
- if not url.startswith("https://pypi.org/"):
479
- msg = f"Invalid URL scheme: {url}"
467
+ from urllib.parse import urlparse
468
+
469
+ parsed = urlparse(url)
470
+
471
+ if parsed.scheme != "https":
472
+ msg = f"Invalid URL scheme '{parsed.scheme}': only HTTPS is allowed"
473
+ raise ValueError(msg)
474
+
475
+ if parsed.netloc != "pypi.org":
476
+ msg = f"Invalid hostname '{parsed.netloc}': only pypi.org is allowed"
477
+ raise ValueError(msg)
478
+
479
+ if not parsed.path.startswith("/pypi/") or not parsed.path.endswith("/json"):
480
+ msg = f"Invalid PyPI API path: {parsed.path}"
480
481
  raise ValueError(msg)
481
482
 
482
483
  def _extract_version_info(self, data: dict[str, t.Any]) -> dict[str, t.Any] | None:
483
- """Extract version information from PyPI response data."""
484
484
  info = data.get("info", {})
485
485
  releases = data.get("releases", {})
486
486
 
@@ -498,14 +498,12 @@ class DependencyMonitorService:
498
498
  }
499
499
 
500
500
  def _get_release_date(self, releases: dict[str, t.Any], version: str) -> str:
501
- """Extract release date for a specific version."""
502
501
  release_info = releases.get(version, [])
503
502
  if release_info:
504
503
  return release_info[0].get("upload_time", "")
505
504
  return ""
506
505
 
507
506
  def _has_breaking_changes(self, version: str) -> bool:
508
- """Determine if version likely has breaking changes based on major version."""
509
507
  return version.split(".")[0] != "0" if "." in version else False
510
508
 
511
509
  def _is_major_version_update(self, current: str, latest: str) -> bool:
@@ -549,69 +547,73 @@ class DependencyMonitorService:
549
547
  self,
550
548
  vulnerabilities: list[DependencyVulnerability],
551
549
  ) -> None:
552
- self.console.print("\n[bold red]🚨 Security Vulnerabilities Found![/bold red]")
553
550
  self.console.print(
554
- "[red]Please update the following packages immediately:[/red]\n",
551
+ "\n[bold red]🚨 Security Vulnerabilities Found ![/ bold red]"
552
+ )
553
+ self.console.print(
554
+ "[red]Please update the following packages immediately: [/ red]\n",
555
555
  )
556
556
 
557
557
  for vuln in vulnerabilities:
558
- self.console.print(f"[red]• {vuln.package} {vuln.installed_version}[/red]")
559
- self.console.print(f" [dim]Vulnerability ID: {vuln.vulnerability_id}[/dim]")
560
- self.console.print(f" [dim]Severity: {vuln.severity.upper()}[/dim]")
558
+ self.console.print(f"[red]• {vuln.package} {vuln.installed_version}[/ red]")
559
+ self.console.print(
560
+ f" [dim]Vulnerability ID: {vuln.vulnerability_id}[/ dim]"
561
+ )
562
+ self.console.print(f" [dim]Severity: {vuln.severity.upper()}[/ dim]")
561
563
  if vuln.patched_version:
562
564
  self.console.print(
563
- f" [green]Fix available: {vuln.patched_version}[/green]",
565
+ f" [green]Fix available: {vuln.patched_version}[/ green]",
564
566
  )
565
567
  if vuln.advisory_url:
566
- self.console.print(f" [dim]More info: {vuln.advisory_url}[/dim]")
568
+ self.console.print(f" [dim]More info: {vuln.advisory_url}[/ dim]")
567
569
  self.console.print()
568
570
 
569
571
  def _report_major_updates(self, major_updates: list[MajorUpdate]) -> None:
570
572
  self.console.print(
571
- "\n[bold yellow]📦 Major Version Updates Available[/bold yellow]",
573
+ "\n[bold yellow]📦 Major Version Updates Available[/ bold yellow]",
572
574
  )
573
575
  self.console.print(
574
- "[yellow]The following packages have major updates:[/yellow]\n",
576
+ "[yellow]The following packages have major updates: [/ yellow]\n",
575
577
  )
576
578
 
577
579
  for update in major_updates:
578
- self.console.print(f"[yellow]• {update.package}[/yellow]")
579
- self.console.print(f" [dim]Current: {update.current_version}[/dim]")
580
- self.console.print(f" [dim]Latest: {update.latest_version}[/dim]")
580
+ self.console.print(f"[yellow]• {update.package}[/ yellow]")
581
+ self.console.print(f" [dim]Current: {update.current_version}[/ dim]")
582
+ self.console.print(f" [dim]Latest: {update.latest_version}[/ dim]")
581
583
  if update.release_date:
582
584
  release_date = update.release_date[:10]
583
- self.console.print(f" [dim]Released: {release_date}[/dim]")
585
+ self.console.print(f" [dim]Released: {release_date}[/ dim]")
584
586
  if update.breaking_changes:
585
- self.console.print(" [red]⚠️ May contain breaking changes[/red]")
587
+ self.console.print(" [red]⚠️ May contain breaking changes[/ red]")
586
588
  self.console.print()
587
589
 
588
590
  self.console.print(
589
- "[dim]Review changelogs before updating to major versions.[/dim]",
591
+ "[dim]Review changelogs before updating to major versions.[/ dim]",
590
592
  )
591
593
 
592
594
  def force_check_updates(
593
595
  self,
594
596
  ) -> tuple[list[DependencyVulnerability], list[MajorUpdate]]:
595
597
  if not self.pyproject_path.exists():
596
- self.console.print("[yellow]⚠️ No pyproject.toml found[/yellow]")
598
+ self.console.print("[yellow]⚠️ No pyproject.toml found[/ yellow]")
597
599
  return [], []
598
600
 
599
- self.console.print("[dim]Parsing dependencies from pyproject.toml...[/dim]")
601
+ self.console.print("[dim]Parsing dependencies from pyproject.toml...[/ dim]")
600
602
  dependencies = self._parse_dependencies()
601
603
  if not dependencies:
602
604
  self.console.print(
603
- "[yellow]⚠️ No dependencies found in pyproject.toml[/yellow]",
605
+ "[yellow]⚠️ No dependencies found in pyproject.toml[/ yellow]",
604
606
  )
605
607
  return [], []
606
608
 
607
609
  self.console.print(
608
- f"[dim]Found {len(dependencies)} dependencies to check[/dim]",
610
+ f"[dim]Found {len(dependencies)} dependencies to check[/ dim]",
609
611
  )
610
612
 
611
- self.console.print("[dim]Checking for security vulnerabilities...[/dim]")
613
+ self.console.print("[dim]Checking for security vulnerabilities...[/ dim]")
612
614
  vulnerabilities = self._check_security_vulnerabilities(dependencies)
613
615
 
614
- self.console.print("[dim]Checking for major version updates...[/dim]")
616
+ self.console.print("[dim]Checking for major version updates...[/ dim]")
615
617
  major_updates = self._check_major_updates(dependencies)
616
618
 
617
619
  return vulnerabilities, major_updates
@@ -136,7 +136,8 @@ class BatchFileOperations:
136
136
 
137
137
  await asyncio.gather(*tasks, return_exceptions=True)
138
138
 
139
- async def _read_single_async(self, path: Path, future: asyncio.Future[str]) -> None:
139
+ @staticmethod
140
+ async def _read_single_async(path: Path, future: asyncio.Future[str]) -> None:
140
141
  try:
141
142
  async with aiofiles.open(path, encoding="utf-8") as f:
142
143
  content = await f.read()
@@ -144,8 +145,8 @@ class BatchFileOperations:
144
145
  except Exception as e:
145
146
  future.set_exception(e)
146
147
 
148
+ @staticmethod
147
149
  async def _write_single_async(
148
- self,
149
150
  path: Path,
150
151
  content: str,
151
152
  future: asyncio.Future[None],
@@ -192,9 +193,6 @@ class EnhancedFileSystemService(FileSystemInterface):
192
193
  return content
193
194
 
194
195
  def write_file(self, path: str | Path, content: str) -> None:
195
- if not isinstance(content, str): # type: ignore[arg-type]
196
- raise TypeError("Content must be a string")
197
-
198
196
  path_obj = Path(path) if isinstance(path, str) else path
199
197
 
200
198
  with LoggingContext("write_file", path=str(path_obj), size=len(content)):
@@ -286,7 +284,8 @@ class EnhancedFileSystemService(FileSystemInterface):
286
284
 
287
285
  await asyncio.gather(*tasks, return_exceptions=True)
288
286
 
289
- def _get_cache_key(self, path: Path) -> str:
287
+ @staticmethod
288
+ def _get_cache_key(path: Path) -> str:
290
289
  path_str = str(path.resolve())
291
290
  return hashlib.md5(path_str.encode(), usedforsecurity=False).hexdigest()
292
291
 
@@ -306,7 +305,8 @@ class EnhancedFileSystemService(FileSystemInterface):
306
305
 
307
306
  return self.cache.get(cache_key)
308
307
 
309
- def _read_file_direct(self, path: Path) -> str:
308
+ @staticmethod
309
+ def _read_file_direct(path: Path) -> str:
310
310
  try:
311
311
  if not path.exists():
312
312
  raise FileError(
@@ -325,7 +325,7 @@ class EnhancedFileSystemService(FileSystemInterface):
325
325
  raise FileError(
326
326
  message=f"Unable to decode file as UTF-8: {path}",
327
327
  details=str(e),
328
- recovery="Ensure file is text-based and UTF-8 encoded",
328
+ recovery="Ensure file is text - based and UTF-8 encoded",
329
329
  ) from e
330
330
  except OSError as e:
331
331
  raise FileError(
@@ -334,7 +334,8 @@ class EnhancedFileSystemService(FileSystemInterface):
334
334
  recovery="Check disk space and file system integrity",
335
335
  ) from e
336
336
 
337
- def _write_file_direct(self, path: Path, content: str) -> None:
337
+ @staticmethod
338
+ def _write_file_direct(path: Path, content: str) -> None:
338
339
  try:
339
340
  try:
340
341
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -360,7 +361,8 @@ class EnhancedFileSystemService(FileSystemInterface):
360
361
  recovery="Check disk space and file system integrity",
361
362
  ) from e
362
363
 
363
- def file_exists(self, path: str | Path) -> bool:
364
+ @staticmethod
365
+ def file_exists(path: str | Path) -> bool:
364
366
  return (Path(path) if isinstance(path, str) else path).exists()
365
367
 
366
368
  def create_directory(self, path: str | Path) -> None:
@@ -394,7 +396,8 @@ class EnhancedFileSystemService(FileSystemInterface):
394
396
  recovery="Check file permissions",
395
397
  ) from e
396
398
 
397
- def list_files(self, path: str | Path, pattern: str = "*") -> Iterator[Path]:
399
+ @staticmethod
400
+ def list_files(path: str | Path, pattern: str = "*") -> Iterator[Path]:
398
401
  path_obj = Path(path) if isinstance(path, str) else path
399
402
 
400
403
  if not path_obj.is_dir():
@@ -110,7 +110,7 @@ class FileHasher:
110
110
  if file_path:
111
111
  stat = file_path.stat() if file_path.exists() else None
112
112
  if stat:
113
- cache_key = f"file_hash:{file_path}:{stat.st_mtime}:{stat.st_size}"
113
+ cache_key = f"file_hash: {file_path}: {stat.st_mtime}: {stat.st_size}"
114
114
  self.cache.file_hash_cache.invalidate(cache_key)
115
115
  else:
116
116
  self.cache.file_hash_cache.clear()
@@ -8,19 +8,9 @@ from crackerjack.errors import ErrorCode, FileError, ResourceError
8
8
  class FileSystemService:
9
9
  @staticmethod
10
10
  def clean_trailing_whitespace_and_newlines(content: str) -> str:
11
- """Clean trailing whitespace from all lines and ensure single trailing newline.
12
-
13
- Args:
14
- content: File content to clean
15
-
16
- Returns:
17
- Cleaned content with no trailing whitespace and single trailing newline
18
- """
19
- # Remove trailing whitespace from each line
20
11
  lines = content.splitlines()
21
12
  cleaned_lines = [line.rstrip() for line in lines]
22
13
 
23
- # Join lines and ensure exactly one trailing newline
24
14
  result = "\n".join(cleaned_lines)
25
15
  if result and not result.endswith("\n"):
26
16
  result += "\n"
@@ -49,7 +39,7 @@ class FileSystemService:
49
39
  message=f"Unable to decode file as UTF-8: {path}",
50
40
  error_code=ErrorCode.FILE_READ_ERROR,
51
41
  details=str(e),
52
- recovery="Ensure file is text-based and UTF-8 encoded",
42
+ recovery="Ensure file is text - based and UTF-8 encoded",
53
43
  ) from e
54
44
  except OSError as e:
55
45
  raise FileError(
@@ -72,7 +62,6 @@ class FileSystemService:
72
62
  recovery="Check disk space and directory permissions",
73
63
  ) from e
74
64
 
75
- # Auto-clean configuration files to prevent pre-commit hook failures
76
65
  if path_obj.name in {".pre-commit-config.yaml", "pyproject.toml"}:
77
66
  content = self.clean_trailing_whitespace_and_newlines(content)
78
67