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
@@ -0,0 +1,382 @@
1
+ import asyncio
2
+ import builtins
3
+ import hashlib
4
+ import typing as t
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+ from threading import Lock
9
+ from weakref import WeakValueDictionary
10
+
11
+ from crackerjack.services.logging import get_logger
12
+
13
+
14
+ @dataclass
15
+ class CacheEntry:
16
+ value: t.Any
17
+ created_at: datetime
18
+ access_count: int = 0
19
+ last_accessed: datetime = field(default_factory=datetime.now)
20
+ ttl_seconds: int = 300
21
+ invalidation_keys: set[str] = field(default_factory=set)
22
+
23
+ def is_expired(self) -> bool:
24
+ if self.ttl_seconds <= 0:
25
+ return False
26
+ return datetime.now() > self.created_at + timedelta(seconds=self.ttl_seconds)
27
+
28
+ def access(self) -> t.Any:
29
+ self.access_count += 1
30
+ self.last_accessed = datetime.now()
31
+ return self.value
32
+
33
+
34
+ @dataclass
35
+ class CacheStats:
36
+ hits: int = 0
37
+ misses: int = 0
38
+ evictions: int = 0
39
+ memory_usage_bytes: int = 0
40
+
41
+ @property
42
+ def hit_ratio(self) -> float:
43
+ total = self.hits + self.misses
44
+ return self.hits / total if total > 0 else 0.0
45
+
46
+
47
+ class PerformanceCache:
48
+ def __init__(
49
+ self,
50
+ max_memory_mb: int = 50,
51
+ default_ttl_seconds: int = 300,
52
+ cleanup_interval_seconds: int = 60,
53
+ ):
54
+ self.max_memory_bytes = max_memory_mb * 1024 * 1024
55
+ self.default_ttl_seconds = default_ttl_seconds
56
+ self.cleanup_interval_seconds = cleanup_interval_seconds
57
+
58
+ self._cache: dict[str, CacheEntry] = {}
59
+ self._lock = Lock()
60
+ self._stats = CacheStats()
61
+ self._logger = get_logger("crackerjack.performance_cache")
62
+ self._cleanup_task: asyncio.Task[None] | None = None
63
+ self._invalidation_map: dict[str, set[str]] = {}
64
+
65
+ self._weak_cache: WeakValueDictionary[str, t.Any] = WeakValueDictionary()
66
+
67
+ async def start(self) -> None:
68
+ if self._cleanup_task is None:
69
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
70
+ self._logger.info("Performance cache started")
71
+
72
+ async def stop(self) -> None:
73
+ if self._cleanup_task:
74
+ self._cleanup_task.cancel()
75
+ try:
76
+ await self._cleanup_task
77
+ except asyncio.CancelledError:
78
+ pass
79
+ self._cleanup_task = None
80
+ self._logger.info("Performance cache stopped")
81
+
82
+ def get(
83
+ self,
84
+ key: str,
85
+ default: t.Any = None,
86
+ ) -> t.Any:
87
+ with self._lock:
88
+ if key not in self._cache:
89
+ self._stats.misses += 1
90
+ return default
91
+
92
+ entry = self._cache[key]
93
+ if entry.is_expired():
94
+ del self._cache[key]
95
+ self._stats.misses += 1
96
+ self._stats.evictions += 1
97
+ return default
98
+
99
+ self._stats.hits += 1
100
+ return entry.access()
101
+
102
+ async def get_async(
103
+ self,
104
+ key: str,
105
+ default: t.Any = None,
106
+ ) -> t.Any:
107
+ return self.get(key, default)
108
+
109
+ def set(
110
+ self,
111
+ key: str,
112
+ value: t.Any,
113
+ ttl_seconds: int | None = None,
114
+ invalidation_keys: set[str] | None = None,
115
+ ) -> None:
116
+ ttl = ttl_seconds if ttl_seconds is not None else self.default_ttl_seconds
117
+ inv_keys = invalidation_keys or set()
118
+
119
+ entry = CacheEntry(
120
+ value=value,
121
+ created_at=datetime.now(),
122
+ ttl_seconds=ttl,
123
+ invalidation_keys=inv_keys,
124
+ )
125
+
126
+ with self._lock:
127
+ self._cache[key] = entry
128
+
129
+ for inv_key in inv_keys:
130
+ if inv_key not in self._invalidation_map:
131
+ self._invalidation_map[inv_key] = set()
132
+ self._invalidation_map[inv_key].add(key)
133
+
134
+ self._check_memory_pressure()
135
+
136
+ async def set_async(
137
+ self,
138
+ key: str,
139
+ value: t.Any,
140
+ ttl_seconds: int | None = None,
141
+ invalidation_keys: builtins.set[str] | None = None,
142
+ ) -> None:
143
+ self.set[t.Any](key, value, ttl_seconds, invalidation_keys)
144
+
145
+ def invalidate(self, invalidation_key: str) -> int:
146
+ with self._lock:
147
+ if invalidation_key not in self._invalidation_map:
148
+ return 0
149
+
150
+ keys_to_remove = self._invalidation_map[invalidation_key].copy()
151
+ count = 0
152
+
153
+ for key in keys_to_remove:
154
+ if key in self._cache:
155
+ del self._cache[key]
156
+ count += 1
157
+ self._stats.evictions += 1
158
+
159
+ del self._invalidation_map[invalidation_key]
160
+
161
+ self._logger.debug(
162
+ f"Invalidated {count} cache entries for key: {invalidation_key}"
163
+ )
164
+ return count
165
+
166
+ def clear(self) -> None:
167
+ with self._lock:
168
+ count = len(self._cache)
169
+ self._cache.clear()
170
+ self._invalidation_map.clear()
171
+ self._stats.evictions += count
172
+ self._logger.info(f"Cleared {count} cache entries")
173
+
174
+ def get_stats(self) -> CacheStats:
175
+ with self._lock:
176
+ stats = CacheStats(
177
+ hits=self._stats.hits,
178
+ misses=self._stats.misses,
179
+ evictions=self._stats.evictions,
180
+ memory_usage_bytes=self._estimate_memory_usage(),
181
+ )
182
+ return stats
183
+
184
+ def _estimate_memory_usage(self) -> int:
185
+ total_size = 0
186
+ for entry in self._cache.values():
187
+ if isinstance(entry.value, str | bytes):
188
+ total_size += len(entry.value)
189
+ elif isinstance(entry.value, list | tuple):
190
+ total_size += len(entry.value) * 100
191
+ elif isinstance(entry.value, dict):
192
+ total_size += len(entry.value) * 200
193
+ else:
194
+ total_size += 1000
195
+
196
+ return total_size
197
+
198
+ def _check_memory_pressure(self) -> None:
199
+ if self._estimate_memory_usage() > self.max_memory_bytes:
200
+ self._evict_lru_entries()
201
+
202
+ def _evict_lru_entries(self) -> None:
203
+ if not self._cache:
204
+ return
205
+
206
+ entries_by_access = sorted(
207
+ self._cache.items(),
208
+ key=lambda x: x[1].last_accessed,
209
+ )
210
+
211
+ evict_count = max(1, len(entries_by_access) // 4)
212
+
213
+ for key, _ in entries_by_access[:evict_count]:
214
+ del self._cache[key]
215
+ self._stats.evictions += 1
216
+
217
+ self._logger.debug(f"Evicted {evict_count} LRU cache entries")
218
+
219
+ async def _cleanup_loop(self) -> None:
220
+ while True:
221
+ try:
222
+ await asyncio.sleep(self.cleanup_interval_seconds)
223
+ self._cleanup_expired_entries()
224
+ except asyncio.CancelledError:
225
+ break
226
+ except Exception as e:
227
+ self._logger.error(f"Error in cache cleanup loop: {e}")
228
+
229
+ def _cleanup_expired_entries(self) -> None:
230
+ with self._lock:
231
+ expired_keys = [
232
+ key for key, entry in self._cache.items() if entry.is_expired()
233
+ ]
234
+
235
+ for key in expired_keys:
236
+ del self._cache[key]
237
+ self._stats.evictions += 1
238
+
239
+ if expired_keys:
240
+ self._logger.debug(f"Cleaned up {len(expired_keys)} expired entries")
241
+
242
+
243
+ class GitOperationCache:
244
+ def __init__(self, cache: PerformanceCache):
245
+ self.cache = cache
246
+ self._logger = get_logger("crackerjack.git_cache")
247
+
248
+ def _make_repo_key(self, repo_path: Path, operation: str, params: str = "") -> str:
249
+ repo_hash = hashlib.md5(
250
+ str(repo_path).encode(), usedforsecurity=False
251
+ ).hexdigest()[:8]
252
+ param_hash = (
253
+ hashlib.md5(params.encode(), usedforsecurity=False).hexdigest()[:8]
254
+ if params
255
+ else ""
256
+ )
257
+ return f"git: {repo_hash}: {operation}: {param_hash}"
258
+
259
+ def get_branch_info(self, repo_path: Path) -> t.Any:
260
+ key = self._make_repo_key(repo_path, "branch_info")
261
+ return self.cache.get(key)
262
+
263
+ def set_branch_info(
264
+ self,
265
+ repo_path: Path,
266
+ branch_info: t.Any,
267
+ ttl_seconds: int = 60,
268
+ ) -> None:
269
+ key = self._make_repo_key(repo_path, "branch_info")
270
+ invalidation_keys = {f"git_repo: {repo_path}"}
271
+ self.cache.set[t.Any](key, branch_info, ttl_seconds, invalidation_keys)
272
+
273
+ def get_file_status(self, repo_path: Path) -> t.Any:
274
+ key = self._make_repo_key(repo_path, "file_status")
275
+ return self.cache.get(key)
276
+
277
+ def set_file_status(
278
+ self,
279
+ repo_path: Path,
280
+ file_status: t.Any,
281
+ ttl_seconds: int = 30,
282
+ ) -> None:
283
+ key = self._make_repo_key(repo_path, "file_status")
284
+ invalidation_keys = {f"git_repo: {repo_path}", "git_files"}
285
+ self.cache.set[t.Any](key, file_status, ttl_seconds, invalidation_keys)
286
+
287
+ def invalidate_repo(self, repo_path: Path) -> None:
288
+ self.cache.invalidate(f"git_repo: {repo_path}")
289
+ self._logger.debug(f"Invalidated git cache for repo: {repo_path}")
290
+
291
+
292
+ class FileSystemCache:
293
+ def __init__(self, cache: PerformanceCache):
294
+ self.cache = cache
295
+ self._logger = get_logger("crackerjack.filesystem_cache")
296
+
297
+ def _make_file_key(self, file_path: Path, operation: str) -> str:
298
+ file_hash = hashlib.md5(
299
+ str(file_path).encode(), usedforsecurity=False
300
+ ).hexdigest()[:8]
301
+ return f"fs: {file_hash}: {operation}"
302
+
303
+ def get_file_stats(self, file_path: Path) -> t.Any:
304
+ key = self._make_file_key(file_path, "stats")
305
+ return self.cache.get(key)
306
+
307
+ def set_file_stats(
308
+ self,
309
+ file_path: Path,
310
+ stats: t.Any,
311
+ ttl_seconds: int = 60,
312
+ ) -> None:
313
+ key = self._make_file_key(file_path, "stats")
314
+ invalidation_keys = {f"file: {file_path}"}
315
+ self.cache.set[t.Any](key, stats, ttl_seconds, invalidation_keys)
316
+
317
+ def invalidate_file(self, file_path: Path) -> None:
318
+ self.cache.invalidate(f"file: {file_path}")
319
+
320
+
321
+ class CommandResultCache:
322
+ def __init__(self, cache: PerformanceCache):
323
+ self.cache = cache
324
+ self._logger = get_logger("crackerjack.command_cache")
325
+
326
+ def _make_command_key(self, command: list[str], cwd: Path | None = None) -> str:
327
+ cmd_str = " ".join(command)
328
+ cwd_str = str(cwd) if cwd else ""
329
+ combined = f"{cmd_str}: {cwd_str}"
330
+ cmd_hash = hashlib.md5(combined.encode(), usedforsecurity=False).hexdigest()[
331
+ :12
332
+ ]
333
+ return f"cmd: {cmd_hash}"
334
+
335
+ def get_command_result(
336
+ self,
337
+ command: list[str],
338
+ cwd: Path | None = None,
339
+ ) -> t.Any:
340
+ key = self._make_command_key(command, cwd)
341
+ return self.cache.get(key)
342
+
343
+ def set_command_result(
344
+ self,
345
+ command: list[str],
346
+ result: t.Any,
347
+ cwd: Path | None = None,
348
+ ttl_seconds: int = 120,
349
+ ) -> None:
350
+ key = self._make_command_key(command, cwd)
351
+ invalidation_keys = {"commands"}
352
+ if cwd:
353
+ invalidation_keys.add(f"cwd: {cwd}")
354
+
355
+ self.cache.set[t.Any](key, result, ttl_seconds, invalidation_keys)
356
+
357
+ def invalidate_commands(self) -> None:
358
+ self.cache.invalidate("commands")
359
+
360
+
361
+ _global_cache: PerformanceCache | None = None
362
+ _cache_lock = Lock()
363
+
364
+
365
+ def get_performance_cache() -> PerformanceCache:
366
+ global _global_cache
367
+ with _cache_lock:
368
+ if _global_cache is None:
369
+ _global_cache = PerformanceCache()
370
+ return _global_cache
371
+
372
+
373
+ def get_git_cache() -> GitOperationCache:
374
+ return GitOperationCache(get_performance_cache())
375
+
376
+
377
+ def get_filesystem_cache() -> FileSystemCache:
378
+ return FileSystemCache(get_performance_cache())
379
+
380
+
381
+ def get_command_cache() -> CommandResultCache:
382
+ return CommandResultCache(get_performance_cache())