crackerjack 0.33.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 (198) 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 +4 -13
  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 +104 -204
  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 +171 -174
  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 +44 -8
  74. crackerjack/managers/test_command_builder.py +1 -15
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +98 -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 +17 -16
  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 +173 -32
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +8 -10
  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 +0 -2
  109. crackerjack/mixins/error_handling.py +1 -70
  110. crackerjack/models/config.py +12 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +122 -122
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  115. crackerjack/monitoring/metrics_collector.py +426 -0
  116. crackerjack/monitoring/regression_prevention.py +8 -8
  117. crackerjack/monitoring/websocket_server.py +643 -0
  118. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  119. crackerjack/orchestration/coverage_improvement.py +3 -3
  120. crackerjack/orchestration/execution_strategies.py +26 -6
  121. crackerjack/orchestration/test_progress_streamer.py +8 -5
  122. crackerjack/plugins/base.py +2 -2
  123. crackerjack/plugins/hooks.py +7 -0
  124. crackerjack/plugins/managers.py +11 -8
  125. crackerjack/security/__init__.py +0 -1
  126. crackerjack/security/audit.py +6 -35
  127. crackerjack/services/anomaly_detector.py +392 -0
  128. crackerjack/services/api_extractor.py +615 -0
  129. crackerjack/services/backup_service.py +2 -2
  130. crackerjack/services/bounded_status_operations.py +15 -152
  131. crackerjack/services/cache.py +127 -1
  132. crackerjack/services/changelog_automation.py +395 -0
  133. crackerjack/services/config.py +15 -9
  134. crackerjack/services/config_merge.py +19 -80
  135. crackerjack/services/config_template.py +506 -0
  136. crackerjack/services/contextual_ai_assistant.py +48 -22
  137. crackerjack/services/coverage_badge_service.py +171 -0
  138. crackerjack/services/coverage_ratchet.py +27 -25
  139. crackerjack/services/debug.py +3 -3
  140. crackerjack/services/dependency_analyzer.py +460 -0
  141. crackerjack/services/dependency_monitor.py +14 -11
  142. crackerjack/services/documentation_generator.py +491 -0
  143. crackerjack/services/documentation_service.py +675 -0
  144. crackerjack/services/enhanced_filesystem.py +6 -5
  145. crackerjack/services/enterprise_optimizer.py +865 -0
  146. crackerjack/services/error_pattern_analyzer.py +676 -0
  147. crackerjack/services/file_hasher.py +1 -1
  148. crackerjack/services/git.py +8 -25
  149. crackerjack/services/health_metrics.py +10 -8
  150. crackerjack/services/heatmap_generator.py +735 -0
  151. crackerjack/services/initialization.py +11 -30
  152. crackerjack/services/input_validator.py +5 -97
  153. crackerjack/services/intelligent_commit.py +327 -0
  154. crackerjack/services/log_manager.py +15 -12
  155. crackerjack/services/logging.py +4 -3
  156. crackerjack/services/lsp_client.py +628 -0
  157. crackerjack/services/memory_optimizer.py +19 -87
  158. crackerjack/services/metrics.py +42 -33
  159. crackerjack/services/parallel_executor.py +9 -67
  160. crackerjack/services/pattern_cache.py +1 -1
  161. crackerjack/services/pattern_detector.py +6 -6
  162. crackerjack/services/performance_benchmarks.py +18 -59
  163. crackerjack/services/performance_cache.py +20 -81
  164. crackerjack/services/performance_monitor.py +27 -95
  165. crackerjack/services/predictive_analytics.py +510 -0
  166. crackerjack/services/quality_baseline.py +234 -0
  167. crackerjack/services/quality_baseline_enhanced.py +646 -0
  168. crackerjack/services/quality_intelligence.py +785 -0
  169. crackerjack/services/regex_patterns.py +605 -524
  170. crackerjack/services/regex_utils.py +43 -123
  171. crackerjack/services/secure_path_utils.py +5 -164
  172. crackerjack/services/secure_status_formatter.py +30 -141
  173. crackerjack/services/secure_subprocess.py +11 -92
  174. crackerjack/services/security.py +9 -41
  175. crackerjack/services/security_logger.py +12 -24
  176. crackerjack/services/server_manager.py +124 -16
  177. crackerjack/services/status_authentication.py +16 -159
  178. crackerjack/services/status_security_manager.py +4 -131
  179. crackerjack/services/thread_safe_status_collector.py +19 -125
  180. crackerjack/services/unified_config.py +21 -13
  181. crackerjack/services/validation_rate_limiter.py +5 -54
  182. crackerjack/services/version_analyzer.py +459 -0
  183. crackerjack/services/version_checker.py +1 -1
  184. crackerjack/services/websocket_resource_limiter.py +10 -144
  185. crackerjack/services/zuban_lsp_service.py +390 -0
  186. crackerjack/slash_commands/__init__.py +2 -7
  187. crackerjack/slash_commands/run.md +2 -2
  188. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  189. crackerjack/tools/validate_regex_patterns.py +19 -48
  190. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +196 -25
  191. crackerjack-0.33.1.dist-info/RECORD +229 -0
  192. crackerjack/CLAUDE.md +0 -207
  193. crackerjack/RULES.md +0 -380
  194. crackerjack/py313.py +0 -234
  195. crackerjack-0.33.0.dist-info/RECORD +0 -187
  196. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  197. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  198. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,506 @@
1
+ import hashlib
2
+ import subprocess
3
+ import typing as t
4
+ from contextlib import suppress
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ import tomli
10
+ import yaml
11
+ from rich.console import Console
12
+
13
+
14
+ @dataclass
15
+ class ConfigUpdateInfo:
16
+ config_type: str
17
+ current_version: str
18
+ latest_version: str
19
+ needs_update: bool
20
+ diff_preview: str = ""
21
+ last_updated: datetime | None = None
22
+
23
+
24
+ @dataclass
25
+ class ConfigVersion:
26
+ version: str
27
+ config_data: dict[str, t.Any]
28
+ dependencies: list[str] = field(default_factory=list)
29
+ description: str = ""
30
+
31
+
32
+ class ConfigTemplateService:
33
+ """Version-based configuration template management service."""
34
+
35
+ def __init__(self, console: Console, pkg_path: Path) -> None:
36
+ self.console = console
37
+ self.pkg_path = pkg_path
38
+ self.templates = self._load_config_templates()
39
+
40
+ def _load_config_templates(self) -> dict[str, ConfigVersion]:
41
+ """Load configuration templates as structured data."""
42
+ return {
43
+ "pre-commit": self._create_precommit_template(),
44
+ "pyproject": self._create_pyproject_template(),
45
+ }
46
+
47
+ def _create_precommit_template(self) -> ConfigVersion:
48
+ """Create pre-commit configuration template."""
49
+ return ConfigVersion(
50
+ version="3.0.0",
51
+ description="Pre-commit hooks configuration with modern tools",
52
+ config_data={"repos": self._build_precommit_repos()},
53
+ )
54
+
55
+ def _create_pyproject_template(self) -> ConfigVersion:
56
+ """Create pyproject.toml configuration template."""
57
+ return ConfigVersion(
58
+ version="1.2.0",
59
+ description="Modern Python project configuration with Ruff and pytest",
60
+ config_data={"tool": self._build_pyproject_tools()},
61
+ )
62
+
63
+ def _build_precommit_repos(self) -> list[dict[str, t.Any]]:
64
+ """Build pre-commit repository configurations."""
65
+ return [
66
+ {
67
+ "repo": "local",
68
+ "hooks": self._build_local_precommit_hooks(),
69
+ },
70
+ {
71
+ "repo": "https://github.com/pre-commit/pre-commit-hooks",
72
+ "rev": "v6.0.0",
73
+ "hooks": self._build_standard_precommit_hooks(),
74
+ },
75
+ ]
76
+
77
+ def _build_local_precommit_hooks(self) -> list[dict[str, t.Any]]:
78
+ """Build local pre-commit hook configurations."""
79
+ return [
80
+ {
81
+ "id": "validate-regex-patterns",
82
+ "name": "validate-regex-patterns",
83
+ "entry": "uv run python -m crackerjack.tools.validate_regex_patterns",
84
+ "language": "system",
85
+ "files": r"\.py$",
86
+ "exclude": r"^\.venv/",
87
+ },
88
+ {
89
+ "id": "skylos",
90
+ "name": "skylos-dead-code-detection",
91
+ "entry": "skylos",
92
+ "language": "system",
93
+ "args": ["crackerjack"],
94
+ "pass_filenames": False,
95
+ "stages": ["pre-push", "manual"],
96
+ },
97
+ {
98
+ "id": "pyright",
99
+ "name": "pyright-type-checking",
100
+ "entry": "uv run pyright",
101
+ "language": "system",
102
+ "files": r"^crackerjack/.*\.py$",
103
+ "exclude": r"^crackerjack/(mcp|plugins)/.*\.py$|crackerjack/code_cleaner\.py$",
104
+ "stages": ["pre-push", "manual"],
105
+ },
106
+ ]
107
+
108
+ def _build_standard_precommit_hooks(self) -> list[dict[str, t.Any]]:
109
+ """Build standard pre-commit hook configurations."""
110
+ exclude_pattern = r"^\.venv/"
111
+ return [
112
+ {
113
+ "id": "trailing-whitespace",
114
+ "name": "trailing-whitespace",
115
+ "exclude": exclude_pattern,
116
+ },
117
+ {
118
+ "id": "end-of-file-fixer",
119
+ "name": "end-of-file-fixer",
120
+ "exclude": exclude_pattern,
121
+ },
122
+ {"id": "check-yaml", "name": "check-yaml", "exclude": exclude_pattern},
123
+ {"id": "check-toml", "name": "check-toml", "exclude": exclude_pattern},
124
+ {
125
+ "id": "check-added-large-files",
126
+ "name": "check-added-large-files",
127
+ "exclude": exclude_pattern,
128
+ },
129
+ ]
130
+
131
+ def _build_pyproject_tools(self) -> dict[str, t.Any]:
132
+ """Build pyproject.toml tool configurations."""
133
+ return {
134
+ "ruff": self._build_ruff_config(),
135
+ "pytest": self._build_pytest_config(),
136
+ }
137
+
138
+ def _build_ruff_config(self) -> dict[str, t.Any]:
139
+ """Build Ruff configuration."""
140
+ return {
141
+ "target-version": "py313",
142
+ "line-length": 88,
143
+ "fix": True,
144
+ "unsafe-fixes": True,
145
+ "show-fixes": True,
146
+ "output-format": "full",
147
+ "format": {"docstring-code-format": True},
148
+ "lint": {
149
+ "extend-select": ["C901", "F", "I", "UP"],
150
+ "ignore": ["E402", "F821"],
151
+ "fixable": ["ALL"],
152
+ },
153
+ }
154
+
155
+ def _build_pytest_config(self) -> dict[str, t.Any]:
156
+ """Build pytest configuration."""
157
+ return {
158
+ "ini_options": {
159
+ "asyncio_mode": "auto",
160
+ "timeout": 300,
161
+ "addopts": "--cov=crackerjack --cov-report=term-missing:skip-covered",
162
+ "testpaths": ["tests"],
163
+ "markers": [
164
+ "unit: marks test as a unit test",
165
+ "integration: marks test as an integration test",
166
+ "no_leaks: detect asyncio task leaks",
167
+ ],
168
+ },
169
+ }
170
+
171
+ def get_template(
172
+ self, config_type: str, version: str | None = None
173
+ ) -> ConfigVersion | None:
174
+ """Get configuration template by type and optional version."""
175
+ if config_type not in self.templates:
176
+ return None
177
+
178
+ template = self.templates[config_type]
179
+ if version and template.version != version:
180
+ return None
181
+
182
+ return template
183
+
184
+ def check_updates(self, project_path: Path) -> dict[str, ConfigUpdateInfo]:
185
+ """Check if newer configuration versions are available."""
186
+ updates = {}
187
+
188
+ version_file = project_path / ".crackerjack-config.yaml"
189
+ current_versions = self._load_current_versions(version_file)
190
+
191
+ for config_type, template in self.templates.items():
192
+ current_version = current_versions.get(config_type, "0.0.0")
193
+ needs_update = self._version_compare(current_version, template.version) < 0
194
+
195
+ update_info = ConfigUpdateInfo(
196
+ config_type=config_type,
197
+ current_version=current_version,
198
+ latest_version=template.version,
199
+ needs_update=needs_update,
200
+ )
201
+
202
+ if needs_update:
203
+ update_info.diff_preview = self._generate_diff_preview(
204
+ config_type, project_path
205
+ )
206
+
207
+ updates[config_type] = update_info
208
+
209
+ return updates
210
+
211
+ def _load_current_versions(self, version_file: Path) -> dict[str, str]:
212
+ """Load current configuration versions from tracking file."""
213
+ if not version_file.exists():
214
+ return {}
215
+
216
+ try:
217
+ with version_file.open() as f:
218
+ data = yaml.safe_load(f)
219
+ if not isinstance(data, dict):
220
+ return {}
221
+ configs = data.get("configs", {})
222
+ if not isinstance(configs, dict):
223
+ return {}
224
+ return {
225
+ name: config.get("version", "0.0.0")
226
+ for name, config in configs.items()
227
+ if isinstance(config, dict)
228
+ }
229
+ except Exception:
230
+ return {}
231
+
232
+ def _version_compare(self, version1: str, version2: str) -> int:
233
+ """Compare two semantic versions. Returns -1, 0, or 1."""
234
+
235
+ def version_tuple(v: str) -> tuple[int, ...]:
236
+ return tuple(int(x) for x in v.split("."))
237
+
238
+ v1_tuple = version_tuple(version1)
239
+ v2_tuple = version_tuple(version2)
240
+
241
+ if v1_tuple < v2_tuple:
242
+ return -1
243
+ elif v1_tuple > v2_tuple:
244
+ return 1
245
+ return 0
246
+
247
+ def _generate_diff_preview(self, config_type: str, project_path: Path) -> str:
248
+ """Generate a preview of changes that would be made."""
249
+ if config_type == "pre-commit":
250
+ config_file = project_path / ".pre-commit-config.yaml"
251
+ elif config_type == "pyproject":
252
+ config_file = project_path / "pyproject.toml"
253
+ else:
254
+ return "Diff preview not available for this config type"
255
+
256
+ if not config_file.exists():
257
+ return f"Would create new {config_file.name} file"
258
+
259
+ try:
260
+ with config_file.open() as f:
261
+ if config_type == "pre-commit":
262
+ current_config = yaml.safe_load(f)
263
+ else:
264
+ content = f.read()
265
+ current_config = tomli.loads(content)
266
+
267
+ template = self.get_template(config_type)
268
+ if not template:
269
+ return "Template not found"
270
+
271
+ return self._create_config_diff(current_config, template.config_data)
272
+ except Exception as e:
273
+ return f"Error generating diff preview: {e}"
274
+
275
+ def _create_config_diff(
276
+ self, current: dict[str, t.Any], new: dict[str, t.Any]
277
+ ) -> str:
278
+ """Create a simple diff between two configurations."""
279
+ changes: list[str] = []
280
+ self._collect_config_changes(current, new, changes)
281
+
282
+ if not changes:
283
+ return "No changes detected"
284
+
285
+ return "\n".join(changes[:10]) # Limit to first 10 changes
286
+
287
+ def _collect_config_changes(
288
+ self,
289
+ current: dict[str, t.Any],
290
+ new: dict[str, t.Any],
291
+ changes: list[str],
292
+ path: str = "",
293
+ ) -> None:
294
+ """Collect configuration changes recursively."""
295
+ self._collect_additions_and_modifications(current, new, changes, path)
296
+ self._collect_removals(current, new, changes, path)
297
+
298
+ def _collect_additions_and_modifications(
299
+ self,
300
+ current: dict[str, t.Any],
301
+ new: dict[str, t.Any],
302
+ changes: list[str],
303
+ path: str,
304
+ ) -> None:
305
+ """Collect additions and modifications in configuration."""
306
+ for key, value in new.items():
307
+ key_path = f"{path}.{key}" if path else key
308
+
309
+ if key not in current:
310
+ changes.append(f"+ Add {key_path}: {value}")
311
+ elif self._is_nested_dict(value, current[key]):
312
+ self._collect_config_changes(current[key], value, changes, key_path)
313
+ elif current[key] != value:
314
+ changes.append(f"~ Change {key_path}: {current[key]} → {value}")
315
+
316
+ def _collect_removals(
317
+ self,
318
+ current: dict[str, t.Any],
319
+ new: dict[str, t.Any],
320
+ changes: list[str],
321
+ path: str,
322
+ ) -> None:
323
+ """Collect removals in configuration."""
324
+ for key in current:
325
+ if key not in new:
326
+ key_path = f"{path}.{key}" if path else key
327
+ changes.append(f"- Remove {key_path}")
328
+
329
+ def _is_nested_dict(self, new_value: t.Any, current_value: t.Any) -> bool:
330
+ """Check if both values are dictionaries for nested comparison."""
331
+ return isinstance(new_value, dict) and isinstance(current_value, dict)
332
+
333
+ def apply_update(
334
+ self,
335
+ config_type: str,
336
+ project_path: Path,
337
+ interactive: bool = False,
338
+ ) -> bool:
339
+ """Apply configuration update to project."""
340
+ template = self.get_template(config_type)
341
+ if not template:
342
+ self.console.print(f"[red]❌[/red] Template not found: {config_type}")
343
+ return False
344
+
345
+ try:
346
+ if config_type == "pre-commit":
347
+ return self._apply_precommit_update(template, project_path, interactive)
348
+ elif config_type == "pyproject":
349
+ return self._apply_pyproject_update(template, project_path, interactive)
350
+ else:
351
+ self.console.print(
352
+ f"[yellow]⚠️[/yellow] Unsupported config type: {config_type}"
353
+ )
354
+ return False
355
+ except Exception as e:
356
+ self.console.print(f"[red]❌[/red] Failed to apply update: {e}")
357
+ return False
358
+
359
+ def _apply_precommit_update(
360
+ self, template: ConfigVersion, project_path: Path, interactive: bool
361
+ ) -> bool:
362
+ """Apply pre-commit configuration update."""
363
+ config_file = project_path / ".pre-commit-config.yaml"
364
+
365
+ if interactive and config_file.exists():
366
+ self.console.print(f"\n[bold cyan]Updating {config_file.name}[/bold cyan]")
367
+ diff = self._generate_diff_preview("pre-commit", project_path)
368
+ self.console.print(f"Changes:\n{diff}")
369
+
370
+ if not self._confirm_update():
371
+ return False
372
+
373
+ try:
374
+ with config_file.open("w") as f:
375
+ yaml.dump(
376
+ template.config_data, f, default_flow_style=False, sort_keys=False
377
+ )
378
+
379
+ self._update_version_tracking(project_path, "pre-commit", template.version)
380
+ self._invalidate_precommit_cache(project_path)
381
+
382
+ self.console.print(
383
+ f"[green]✅[/green] Updated {config_file.name} to version {template.version}"
384
+ )
385
+ return True
386
+ except Exception as e:
387
+ self.console.print(f"[red]❌[/red] Failed to write config: {e}")
388
+ return False
389
+
390
+ def _apply_pyproject_update(
391
+ self, template: ConfigVersion, project_path: Path, interactive: bool
392
+ ) -> bool:
393
+ """Apply pyproject.toml configuration update."""
394
+ config_file = project_path / "pyproject.toml"
395
+
396
+ if not config_file.exists():
397
+ self.console.print(
398
+ f"[yellow]⚠️[/yellow] pyproject.toml not found at {project_path}"
399
+ )
400
+ return False
401
+
402
+ if interactive:
403
+ self.console.print(f"\n[bold cyan]Updating {config_file.name}[/bold cyan]")
404
+ diff = self._generate_diff_preview("pyproject", project_path)
405
+ self.console.print(f"Changes:\n{diff}")
406
+
407
+ if not self._confirm_update():
408
+ return False
409
+
410
+ try:
411
+ # Read existing config
412
+ with config_file.open() as f:
413
+ content = f.read()
414
+ existing_config = tomli.loads(content)
415
+
416
+ # Merge tool sections from template
417
+ if "tool" not in existing_config:
418
+ existing_config["tool"] = {}
419
+
420
+ for tool_name, tool_config in template.config_data.get("tool", {}).items():
421
+ existing_config["tool"][tool_name] = tool_config
422
+
423
+ # Write back using tomli_w
424
+ from tomli_w import dumps
425
+
426
+ updated_content = dumps(existing_config)
427
+
428
+ with config_file.open("w") as f:
429
+ f.write(updated_content)
430
+
431
+ self._update_version_tracking(project_path, "pyproject", template.version)
432
+
433
+ self.console.print(
434
+ f"[green]✅[/green] Updated {config_file.name} to version {template.version}"
435
+ )
436
+ return True
437
+ except Exception as e:
438
+ self.console.print(f"[red]❌[/red] Failed to update pyproject.toml: {e}")
439
+ return False
440
+
441
+ def _confirm_update(self) -> bool:
442
+ """Ask user to confirm update."""
443
+ try:
444
+ response = input("\nApply this update? [y/N]: ").strip().lower()
445
+ return response in ("y", "yes")
446
+ except (EOFError, KeyboardInterrupt):
447
+ return False
448
+
449
+ def _update_version_tracking(
450
+ self, project_path: Path, config_type: str, version: str
451
+ ) -> None:
452
+ """Update version tracking file."""
453
+ version_file = project_path / ".crackerjack-config.yaml"
454
+
455
+ data = {"version": "1.0.0", "configs": {}}
456
+ if version_file.exists():
457
+ with suppress(Exception):
458
+ with version_file.open() as f:
459
+ existing_data = yaml.safe_load(f)
460
+ if isinstance(existing_data, dict):
461
+ data = existing_data
462
+
463
+ if "configs" not in data:
464
+ data["configs"] = {}
465
+
466
+ data["configs"][config_type] = {
467
+ "version": version,
468
+ "last_updated": datetime.now().isoformat(),
469
+ }
470
+
471
+ with suppress(Exception):
472
+ with version_file.open("w") as f:
473
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
474
+
475
+ def _invalidate_precommit_cache(self, project_path: Path) -> None:
476
+ """Invalidate pre-commit cache to ensure fresh environment."""
477
+ with suppress(Exception):
478
+ subprocess.run(
479
+ ["pre-commit", "clean"],
480
+ cwd=project_path,
481
+ capture_output=True,
482
+ text=True,
483
+ timeout=30,
484
+ )
485
+ subprocess.run(
486
+ ["pre-commit", "install"],
487
+ cwd=project_path,
488
+ capture_output=True,
489
+ text=True,
490
+ timeout=30,
491
+ )
492
+
493
+ def get_config_hash(self, config_path: Path) -> str:
494
+ """Generate hash of configuration file for cache invalidation."""
495
+ if not config_path.exists():
496
+ return ""
497
+
498
+ try:
499
+ content = config_path.read_text()
500
+ return hashlib.sha256(content.encode()).hexdigest()[:16]
501
+ except Exception:
502
+ return ""
503
+
504
+ def list_available_templates(self) -> dict[str, str]:
505
+ """List all available configuration templates."""
506
+ return {name: template.description for name, template in self.templates.items()}
@@ -2,6 +2,7 @@ import json
2
2
  import subprocess
3
3
  import time
4
4
  import tomllib
5
+ import typing as t
5
6
  from dataclasses import dataclass, field
6
7
  from pathlib import Path
7
8
 
@@ -127,7 +128,7 @@ class ContextualAIAssistant:
127
128
  category="testing",
128
129
  priority="medium",
129
130
  title="Progress Toward 100 % Coverage",
130
- description=f"Current coverage: {context.test_coverage: .1f}%. Next milestone: {next_milestone}% on the journey to 100 %.",
131
+ description=f"Current coverage: {context.test_coverage:.1f}%. Next milestone: {next_milestone}% on the journey to 100%.",
131
132
  action_command="python -m crackerjack -t",
132
133
  reasoning="Coverage ratchet system prevents regression and targets 100 % coverage incrementally",
133
134
  confidence=0.85,
@@ -149,7 +150,7 @@ class ContextualAIAssistant:
149
150
  priority="high",
150
151
  title="Fix Lint Errors",
151
152
  description=f"Found {context.lint_errors_count} lint errors that should be addressed.",
152
- action_command="python -m crackerjack --ai-agent",
153
+ action_command="python -m crackerjack --ai-fix",
153
154
  reasoning="High lint error count indicates technical debt and potential bugs",
154
155
  confidence=0.95,
155
156
  ),
@@ -297,7 +298,7 @@ class ContextualAIAssistant:
297
298
 
298
299
  def _determine_project_size(self) -> str:
299
300
  try:
300
- python_files = list(self.project_root.rglob("*.py"))
301
+ python_files = list[t.Any](self.project_root.rglob("*.py"))
301
302
  if len(python_files) < 10:
302
303
  return "small"
303
304
  if len(python_files) < 50:
@@ -481,34 +482,59 @@ class ContextualAIAssistant:
481
482
  10 - int(rec.confidence * 10)
482
483
  )
483
484
  self.console.print(
484
- f" [dim]Confidence: [{confidence_bar}] {rec.confidence: .1 %}[/ dim]",
485
+ f" [dim]Confidence: [{confidence_bar}] {rec.confidence:.1%}[/ dim]",
485
486
  )
486
487
 
487
488
  if i < len(recommendations):
488
489
  self.console.print()
489
490
 
490
491
  def get_quick_help(self, query: str) -> str:
492
+ """Get quick help for common queries using keyword matching."""
491
493
  query_lower = query.lower()
492
494
 
493
- if "coverage" in query_lower:
494
- return "Check test coverage with: python -m crackerjack -t\nView HTML report: uv run coverage html"
495
+ # Define help responses with keywords
496
+ help_mapping = self._get_help_keyword_mapping()
495
497
 
496
- if "security" in query_lower or "vulnerabilit" in query_lower:
497
- return "Check security with: python -m crackerjack --check-dependencies\nRun security audit: uv run bandit -r ."
498
+ # Find the first matching help response
499
+ for keywords, response in help_mapping:
500
+ if self._query_contains_keywords(query_lower, keywords):
501
+ return response
498
502
 
499
- if "lint" in query_lower or "format" in query_lower:
500
- return "Fix code style with: python -m crackerjack\nFor AI-powered fixes: python -m crackerjack --ai-agent"
503
+ return "For full help, run: python -m crackerjack --help\nFor AI assistance: python -m crackerjack --ai-fix"
501
504
 
502
- if "test" in query_lower:
503
- return "Run tests with: python -m crackerjack -t\nFor AI-powered test fixes: python -m crackerjack --ai-agent -t"
504
-
505
- if "publish" in query_lower or "release" in query_lower:
506
- return "Publish to PyPI: python -m crackerjack -p patch\nBump version only: python -m crackerjack -b patch"
507
-
508
- if "clean" in query_lower:
509
- return "Clean code: python -m crackerjack -x\nNote: Resolve TODOs first before cleaning"
510
-
511
- if "dashboard" in query_lower or "monitor" in query_lower:
512
- return "Start monitoring dashboard: python -m crackerjack --dashboard\nStart WebSocket server: python -m crackerjack --start-websocket-server"
505
+ def _get_help_keyword_mapping(self) -> list[tuple[list[str], str]]:
506
+ """Get mapping of keywords to help responses."""
507
+ return [
508
+ (
509
+ ["coverage"],
510
+ "Check test coverage with: python -m crackerjack -t\nView HTML report: uv run coverage html",
511
+ ),
512
+ (
513
+ ["security", "vulnerabilit"],
514
+ "Check security with: python -m crackerjack --check-dependencies\nRun security audit: uv run bandit -r .",
515
+ ),
516
+ (
517
+ ["lint", "format"],
518
+ "Fix code style with: python -m crackerjack\nFor AI-powered fixes: python -m crackerjack --ai-fix",
519
+ ),
520
+ (
521
+ ["test"],
522
+ "Run tests with: python -m crackerjack -t\nFor AI-powered test fixes: python -m crackerjack --ai-fix -t",
523
+ ),
524
+ (
525
+ ["publish", "release"],
526
+ "Publish to PyPI: python -m crackerjack -p patch\nBump version only: python -m crackerjack -b patch",
527
+ ),
528
+ (
529
+ ["clean"],
530
+ "Clean code: python -m crackerjack -x\nNote: Resolve TODOs first before cleaning",
531
+ ),
532
+ (
533
+ ["dashboard", "monitor"],
534
+ "Start monitoring dashboard: python -m crackerjack --dashboard\nStart WebSocket server: python -m crackerjack --start-websocket-server",
535
+ ),
536
+ ]
513
537
 
514
- return "For full help, run: python -m crackerjack --help\nFor AI assistance: python -m crackerjack --ai-agent"
538
+ def _query_contains_keywords(self, query: str, keywords: list[str]) -> bool:
539
+ """Check if query contains any of the specified keywords."""
540
+ return any(keyword in query for keyword in keywords)