crackerjack 0.37.9__py3-none-any.whl → 0.45.2__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.
- crackerjack/README.md +19 -0
- crackerjack/__init__.py +30 -1
- crackerjack/__main__.py +342 -1263
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +27 -5
- crackerjack/adapters/_output_paths.py +167 -0
- crackerjack/adapters/_qa_adapter_base.py +309 -0
- crackerjack/adapters/_tool_adapter_base.py +706 -0
- crackerjack/adapters/ai/README.md +65 -0
- crackerjack/adapters/ai/__init__.py +5 -0
- crackerjack/adapters/ai/claude.py +853 -0
- crackerjack/adapters/complexity/README.md +53 -0
- crackerjack/adapters/complexity/__init__.py +10 -0
- crackerjack/adapters/complexity/complexipy.py +641 -0
- crackerjack/adapters/dependency/__init__.py +22 -0
- crackerjack/adapters/dependency/pip_audit.py +418 -0
- crackerjack/adapters/format/README.md +72 -0
- crackerjack/adapters/format/__init__.py +11 -0
- crackerjack/adapters/format/mdformat.py +313 -0
- crackerjack/adapters/format/ruff.py +516 -0
- crackerjack/adapters/lint/README.md +47 -0
- crackerjack/adapters/lint/__init__.py +11 -0
- crackerjack/adapters/lint/codespell.py +273 -0
- crackerjack/adapters/lsp/README.md +49 -0
- crackerjack/adapters/lsp/__init__.py +27 -0
- crackerjack/adapters/{rust_tool_manager.py → lsp/_manager.py} +3 -3
- crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
- crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
- crackerjack/adapters/refactor/README.md +59 -0
- crackerjack/adapters/refactor/__init__.py +12 -0
- crackerjack/adapters/refactor/creosote.py +318 -0
- crackerjack/adapters/refactor/refurb.py +406 -0
- crackerjack/adapters/refactor/skylos.py +494 -0
- crackerjack/adapters/sast/README.md +132 -0
- crackerjack/adapters/sast/__init__.py +32 -0
- crackerjack/adapters/sast/_base.py +201 -0
- crackerjack/adapters/sast/bandit.py +423 -0
- crackerjack/adapters/sast/pyscn.py +405 -0
- crackerjack/adapters/sast/semgrep.py +241 -0
- crackerjack/adapters/security/README.md +111 -0
- crackerjack/adapters/security/__init__.py +17 -0
- crackerjack/adapters/security/gitleaks.py +339 -0
- crackerjack/adapters/type/README.md +52 -0
- crackerjack/adapters/type/__init__.py +12 -0
- crackerjack/adapters/type/pyrefly.py +402 -0
- crackerjack/adapters/type/ty.py +402 -0
- crackerjack/adapters/type/zuban.py +522 -0
- crackerjack/adapters/utility/README.md +51 -0
- crackerjack/adapters/utility/__init__.py +10 -0
- crackerjack/adapters/utility/checks.py +884 -0
- crackerjack/agents/README.md +264 -0
- crackerjack/agents/__init__.py +40 -12
- crackerjack/agents/base.py +1 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +49 -53
- crackerjack/agents/dry_agent.py +187 -3
- crackerjack/agents/enhanced_coordinator.py +279 -0
- crackerjack/agents/enhanced_proactive_agent.py +185 -0
- crackerjack/agents/error_middleware.py +53 -0
- crackerjack/agents/formatting_agent.py +6 -8
- crackerjack/agents/helpers/__init__.py +9 -0
- crackerjack/agents/helpers/performance/__init__.py +22 -0
- crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
- crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
- crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
- crackerjack/agents/helpers/refactoring/__init__.py +22 -0
- crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
- crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
- crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
- crackerjack/agents/helpers/test_creation/__init__.py +19 -0
- crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
- crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
- crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
- crackerjack/agents/performance_agent.py +121 -1152
- crackerjack/agents/refactoring_agent.py +156 -655
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +19 -1605
- crackerjack/api.py +5 -7
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +1 -1
- crackerjack/cli/cache_handlers.py +23 -18
- crackerjack/cli/cache_handlers_enhanced.py +1 -4
- crackerjack/cli/facade.py +70 -8
- crackerjack/cli/formatting.py +13 -0
- crackerjack/cli/handlers/__init__.py +85 -0
- crackerjack/cli/handlers/advanced.py +103 -0
- crackerjack/cli/handlers/ai_features.py +62 -0
- crackerjack/cli/handlers/analytics.py +479 -0
- crackerjack/cli/handlers/changelog.py +271 -0
- crackerjack/cli/handlers/config_handlers.py +16 -0
- crackerjack/cli/handlers/coverage.py +84 -0
- crackerjack/cli/handlers/documentation.py +280 -0
- crackerjack/cli/handlers/main_handlers.py +497 -0
- crackerjack/cli/handlers/monitoring.py +371 -0
- crackerjack/cli/handlers.py +249 -49
- crackerjack/cli/interactive.py +8 -5
- crackerjack/cli/options.py +203 -110
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +60 -24
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +256 -0
- crackerjack/config/global_lock_config.py +191 -54
- crackerjack/config/hooks.py +188 -16
- crackerjack/config/loader.py +239 -0
- crackerjack/config/settings.py +141 -0
- crackerjack/config/tool_commands.py +331 -0
- crackerjack/core/README.md +393 -0
- crackerjack/core/async_workflow_orchestrator.py +79 -53
- crackerjack/core/autofix_coordinator.py +22 -9
- crackerjack/core/container.py +10 -9
- crackerjack/core/enhanced_container.py +9 -9
- crackerjack/core/performance.py +1 -1
- crackerjack/core/performance_monitor.py +5 -3
- crackerjack/core/phase_coordinator.py +1018 -634
- crackerjack/core/proactive_workflow.py +3 -3
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +167 -23
- crackerjack/core/session_coordinator.py +187 -382
- crackerjack/core/timeout_manager.py +161 -44
- crackerjack/core/workflow/__init__.py +21 -0
- crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
- crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
- crackerjack/core/workflow/workflow_issue_parser.py +714 -0
- crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
- crackerjack/core/workflow/workflow_security_gates.py +400 -0
- crackerjack/core/workflow_orchestrator.py +1247 -953
- crackerjack/data/README.md +11 -0
- crackerjack/data/__init__.py +8 -0
- crackerjack/data/models.py +79 -0
- crackerjack/data/repository.py +210 -0
- crackerjack/decorators/README.md +180 -0
- crackerjack/decorators/__init__.py +35 -0
- crackerjack/decorators/error_handling.py +649 -0
- crackerjack/decorators/error_handling_decorators.py +334 -0
- crackerjack/decorators/helpers.py +58 -0
- crackerjack/decorators/patterns.py +281 -0
- crackerjack/decorators/utils.py +58 -0
- crackerjack/docs/README.md +11 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/ai_templates.py +1 -1
- crackerjack/documentation/dual_output_generator.py +11 -9
- crackerjack/documentation/reference_generator.py +104 -59
- crackerjack/dynamic_config.py +52 -61
- crackerjack/errors.py +1 -1
- crackerjack/events/README.md +11 -0
- crackerjack/events/__init__.py +16 -0
- crackerjack/events/telemetry.py +175 -0
- crackerjack/events/workflow_bus.py +346 -0
- crackerjack/exceptions/README.md +301 -0
- crackerjack/exceptions/__init__.py +5 -0
- crackerjack/exceptions/config.py +4 -0
- crackerjack/exceptions/tool_execution_error.py +245 -0
- crackerjack/executors/README.md +591 -0
- crackerjack/executors/__init__.py +2 -0
- crackerjack/executors/async_hook_executor.py +539 -77
- crackerjack/executors/cached_hook_executor.py +3 -3
- crackerjack/executors/hook_executor.py +967 -102
- crackerjack/executors/hook_lock_manager.py +31 -22
- crackerjack/executors/individual_hook_executor.py +66 -32
- crackerjack/executors/lsp_aware_hook_executor.py +136 -57
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +23 -7
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +8 -9
- crackerjack/intelligence/README.md +557 -0
- crackerjack/interactive.py +37 -10
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/async_hook_manager.py +41 -57
- crackerjack/managers/hook_manager.py +449 -79
- crackerjack/managers/publish_manager.py +81 -36
- crackerjack/managers/test_command_builder.py +290 -12
- crackerjack/managers/test_executor.py +93 -8
- crackerjack/managers/test_manager.py +1082 -75
- crackerjack/managers/test_progress.py +118 -26
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/cache.py +25 -2
- crackerjack/mcp/client_runner.py +35 -18
- crackerjack/mcp/context.py +9 -9
- crackerjack/mcp/dashboard.py +24 -8
- crackerjack/mcp/enhanced_progress_monitor.py +34 -23
- crackerjack/mcp/file_monitor.py +27 -6
- crackerjack/mcp/progress_components.py +45 -34
- crackerjack/mcp/progress_monitor.py +6 -9
- crackerjack/mcp/rate_limiter.py +11 -7
- crackerjack/mcp/server.py +2 -0
- crackerjack/mcp/server_core.py +187 -55
- crackerjack/mcp/service_watchdog.py +12 -9
- crackerjack/mcp/task_manager.py +2 -2
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +2 -0
- crackerjack/mcp/tools/core_tools.py +75 -52
- crackerjack/mcp/tools/execution_tools.py +87 -31
- crackerjack/mcp/tools/intelligence_tools.py +2 -2
- crackerjack/mcp/tools/proactive_tools.py +1 -1
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +180 -132
- crackerjack/mcp/tools/workflow_executor.py +87 -46
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/app.py +11 -1
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +27 -4
- crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
- crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
- crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
- crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
- crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
- crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
- crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
- crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
- crackerjack/mcp/websocket/monitoring/factory.py +109 -0
- crackerjack/mcp/websocket/monitoring/filters.py +10 -0
- crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
- crackerjack/mcp/websocket/monitoring/models.py +90 -0
- crackerjack/mcp/websocket/monitoring/utils.py +171 -0
- crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
- crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
- crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
- crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
- crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
- crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
- crackerjack/mcp/websocket/monitoring_endpoints.py +16 -2930
- crackerjack/mcp/websocket/server.py +1 -3
- crackerjack/mcp/websocket/websocket_handler.py +107 -6
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +10 -1
- crackerjack/models/config.py +639 -22
- crackerjack/models/config_adapter.py +6 -6
- crackerjack/models/protocols.py +1167 -23
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +19 -3
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/ai_agent_watchdog.py +5 -4
- crackerjack/monitoring/metrics_collector.py +4 -3
- crackerjack/monitoring/regression_prevention.py +4 -3
- crackerjack/monitoring/websocket_server.py +4 -241
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +20 -67
- crackerjack/orchestration/cache/README.md +312 -0
- crackerjack/orchestration/cache/__init__.py +37 -0
- crackerjack/orchestration/cache/memory_cache.py +338 -0
- crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
- crackerjack/orchestration/config.py +297 -0
- crackerjack/orchestration/coverage_improvement.py +13 -6
- crackerjack/orchestration/execution_strategies.py +6 -6
- crackerjack/orchestration/hook_orchestrator.py +1398 -0
- crackerjack/orchestration/strategies/README.md +401 -0
- crackerjack/orchestration/strategies/__init__.py +39 -0
- crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
- crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
- crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
- crackerjack/orchestration/test_progress_streamer.py +1 -1
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/hooks.py +3 -2
- crackerjack/plugins/loader.py +3 -3
- crackerjack/plugins/managers.py +1 -1
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +8 -21
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
- crackerjack/services/ai/embeddings.py +444 -0
- crackerjack/services/ai/intelligent_commit.py +328 -0
- crackerjack/services/ai/predictive_analytics.py +510 -0
- crackerjack/services/api_extractor.py +5 -3
- crackerjack/services/bounded_status_operations.py +45 -5
- crackerjack/services/cache.py +249 -318
- crackerjack/services/changelog_automation.py +7 -3
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +83 -39
- crackerjack/services/config_merge.py +9 -6
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +13 -26
- crackerjack/services/coverage_badge_service.py +6 -4
- crackerjack/services/coverage_ratchet.py +53 -27
- crackerjack/services/debug.py +18 -7
- crackerjack/services/dependency_analyzer.py +4 -4
- crackerjack/services/dependency_monitor.py +13 -13
- crackerjack/services/documentation_generator.py +4 -2
- crackerjack/services/documentation_service.py +62 -33
- crackerjack/services/enhanced_filesystem.py +81 -27
- crackerjack/services/enterprise_optimizer.py +1 -1
- crackerjack/services/error_pattern_analyzer.py +10 -10
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +5 -7
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +80 -109
- crackerjack/services/git.py +99 -5
- crackerjack/services/health_metrics.py +4 -6
- crackerjack/services/heatmap_generator.py +12 -3
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +101 -49
- crackerjack/services/log_manager.py +2 -2
- crackerjack/services/logging.py +120 -68
- crackerjack/services/lsp_client.py +12 -12
- crackerjack/services/memory_optimizer.py +27 -22
- crackerjack/services/monitoring/README.md +30 -0
- crackerjack/services/monitoring/__init__.py +9 -0
- crackerjack/services/monitoring/dependency_monitor.py +678 -0
- crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
- crackerjack/services/monitoring/health_metrics.py +716 -0
- crackerjack/services/monitoring/metrics.py +587 -0
- crackerjack/services/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
- crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
- crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
- crackerjack/services/parallel_executor.py +166 -55
- crackerjack/services/patterns/__init__.py +142 -0
- crackerjack/services/patterns/agents.py +107 -0
- crackerjack/services/patterns/code/__init__.py +15 -0
- crackerjack/services/patterns/code/detection.py +118 -0
- crackerjack/services/patterns/code/imports.py +107 -0
- crackerjack/services/patterns/code/paths.py +159 -0
- crackerjack/services/patterns/code/performance.py +119 -0
- crackerjack/services/patterns/code/replacement.py +36 -0
- crackerjack/services/patterns/core.py +212 -0
- crackerjack/services/patterns/documentation/__init__.py +14 -0
- crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
- crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
- crackerjack/services/patterns/documentation/docstrings.py +89 -0
- crackerjack/services/patterns/formatting.py +226 -0
- crackerjack/services/patterns/operations.py +339 -0
- crackerjack/services/patterns/security/__init__.py +23 -0
- crackerjack/services/patterns/security/code_injection.py +122 -0
- crackerjack/services/patterns/security/credentials.py +190 -0
- crackerjack/services/patterns/security/path_traversal.py +221 -0
- crackerjack/services/patterns/security/unsafe_operations.py +216 -0
- crackerjack/services/patterns/templates.py +62 -0
- crackerjack/services/patterns/testing/__init__.py +18 -0
- crackerjack/services/patterns/testing/error_patterns.py +107 -0
- crackerjack/services/patterns/testing/pytest_output.py +126 -0
- crackerjack/services/patterns/tool_output/__init__.py +16 -0
- crackerjack/services/patterns/tool_output/bandit.py +72 -0
- crackerjack/services/patterns/tool_output/other.py +97 -0
- crackerjack/services/patterns/tool_output/pyright.py +67 -0
- crackerjack/services/patterns/tool_output/ruff.py +44 -0
- crackerjack/services/patterns/url_sanitization.py +114 -0
- crackerjack/services/patterns/utilities.py +42 -0
- crackerjack/services/patterns/utils.py +339 -0
- crackerjack/services/patterns/validation.py +46 -0
- crackerjack/services/patterns/versioning.py +62 -0
- crackerjack/services/predictive_analytics.py +21 -8
- crackerjack/services/profiler.py +280 -0
- crackerjack/services/quality/README.md +415 -0
- crackerjack/services/quality/__init__.py +11 -0
- crackerjack/services/quality/anomaly_detector.py +392 -0
- crackerjack/services/quality/pattern_cache.py +333 -0
- crackerjack/services/quality/pattern_detector.py +479 -0
- crackerjack/services/quality/qa_orchestrator.py +491 -0
- crackerjack/services/{quality_baseline.py → quality/quality_baseline.py} +163 -2
- crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
- crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
- crackerjack/services/regex_patterns.py +58 -2987
- crackerjack/services/regex_utils.py +55 -29
- crackerjack/services/secure_status_formatter.py +42 -15
- crackerjack/services/secure_subprocess.py +35 -2
- crackerjack/services/security.py +16 -8
- crackerjack/services/server_manager.py +40 -51
- crackerjack/services/smart_scheduling.py +46 -6
- crackerjack/services/status_authentication.py +3 -3
- crackerjack/services/thread_safe_status_collector.py +1 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +9 -5
- crackerjack/services/unified_config.py +43 -351
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +6 -4
- crackerjack/services/version_checker.py +14 -8
- crackerjack/services/zuban_lsp_service.py +5 -4
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/init.md +2 -12
- crackerjack/slash_commands/run.md +84 -50
- crackerjack/tools/README.md +11 -0
- crackerjack/tools/__init__.py +30 -0
- crackerjack/tools/_git_utils.py +105 -0
- crackerjack/tools/check_added_large_files.py +139 -0
- crackerjack/tools/check_ast.py +105 -0
- crackerjack/tools/check_json.py +103 -0
- crackerjack/tools/check_jsonschema.py +297 -0
- crackerjack/tools/check_toml.py +103 -0
- crackerjack/tools/check_yaml.py +110 -0
- crackerjack/tools/codespell_wrapper.py +72 -0
- crackerjack/tools/end_of_file_fixer.py +202 -0
- crackerjack/tools/format_json.py +128 -0
- crackerjack/tools/mdformat_wrapper.py +114 -0
- crackerjack/tools/trailing_whitespace.py +198 -0
- crackerjack/tools/validate_regex_patterns.py +7 -3
- crackerjack/ui/README.md +11 -0
- crackerjack/ui/dashboard_renderer.py +28 -0
- crackerjack/ui/templates/README.md +11 -0
- crackerjack/utils/console_utils.py +13 -0
- crackerjack/utils/dependency_guard.py +230 -0
- crackerjack/utils/retry_utils.py +275 -0
- crackerjack/workflows/README.md +590 -0
- crackerjack/workflows/__init__.py +46 -0
- crackerjack/workflows/actions.py +811 -0
- crackerjack/workflows/auto_fix.py +444 -0
- crackerjack/workflows/container_builder.py +499 -0
- crackerjack/workflows/definitions.py +443 -0
- crackerjack/workflows/engine.py +177 -0
- crackerjack/workflows/event_bridge.py +242 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack/managers/test_manager_backup.py +0 -1075
- crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
- crackerjack/mixins/__init__.py +0 -3
- crackerjack/mixins/error_handling.py +0 -145
- crackerjack/services/config.py +0 -358
- crackerjack/ui/server_panels.py +0 -125
- crackerjack-0.37.9.dist-info/RECORD +0 -231
- /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
- /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""QA Orchestrator service for coordinating quality assurance checks.
|
|
2
|
+
|
|
3
|
+
This service coordinates multiple QA adapters, handles parallel execution,
|
|
4
|
+
caching, and result aggregation. It replaces the pre-commit hook orchestration
|
|
5
|
+
with native ACB-based quality checks.
|
|
6
|
+
|
|
7
|
+
ACB Patterns:
|
|
8
|
+
- Service implements QAOrchestratorProtocol from models.protocols
|
|
9
|
+
- Async execution throughout
|
|
10
|
+
- Proper error handling and logging
|
|
11
|
+
- Graceful degradation on adapter failures
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import contextlib
|
|
18
|
+
import hashlib
|
|
19
|
+
import typing as t
|
|
20
|
+
from datetime import datetime, timedelta
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
from acb.depends import depends
|
|
25
|
+
|
|
26
|
+
from crackerjack.models.protocols import QAAdapterProtocol
|
|
27
|
+
from crackerjack.models.qa_config import QACheckConfig, QAOrchestratorConfig
|
|
28
|
+
from crackerjack.models.qa_results import QAResult, QAResultStatus
|
|
29
|
+
|
|
30
|
+
if t.TYPE_CHECKING:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class QAOrchestrator:
|
|
35
|
+
"""Orchestrates multiple QA adapters for comprehensive quality checking.
|
|
36
|
+
|
|
37
|
+
Coordinates execution of all registered QA adapters:
|
|
38
|
+
- Parallel execution with configurable concurrency
|
|
39
|
+
- Stage-based execution (fast vs comprehensive)
|
|
40
|
+
- Result caching for performance
|
|
41
|
+
- Formatter-first execution order
|
|
42
|
+
- Incremental checking support
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
```python
|
|
46
|
+
# Initialize orchestrator
|
|
47
|
+
config = QAOrchestratorConfig(
|
|
48
|
+
project_root=Path.cwd(),
|
|
49
|
+
max_parallel_checks=4,
|
|
50
|
+
enable_caching=True,
|
|
51
|
+
fail_fast=False,
|
|
52
|
+
)
|
|
53
|
+
orchestrator = QAOrchestrator(config)
|
|
54
|
+
|
|
55
|
+
# Register adapters
|
|
56
|
+
await orchestrator.register_adapter(ruff_adapter)
|
|
57
|
+
await orchestrator.register_adapter(bandit_adapter)
|
|
58
|
+
await orchestrator.register_adapter(zuban_adapter)
|
|
59
|
+
|
|
60
|
+
# Run fast stage checks
|
|
61
|
+
results = await orchestrator.run_checks(stage="fast")
|
|
62
|
+
|
|
63
|
+
# Run comprehensive checks
|
|
64
|
+
results = await orchestrator.run_checks(stage="comprehensive")
|
|
65
|
+
|
|
66
|
+
# Run all checks
|
|
67
|
+
all_results = await orchestrator.run_all_checks()
|
|
68
|
+
```
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, config: QAOrchestratorConfig) -> None:
|
|
72
|
+
"""Initialize QA orchestrator.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
config: Orchestrator configuration
|
|
76
|
+
"""
|
|
77
|
+
self.config = config
|
|
78
|
+
self._adapters: dict[str, QAAdapterProtocol] = {}
|
|
79
|
+
self._cache: dict[str, tuple[QAResult, datetime]] = {}
|
|
80
|
+
self._semaphore = asyncio.Semaphore(config.max_parallel_checks)
|
|
81
|
+
|
|
82
|
+
async def register_adapter(self, adapter: QAAdapterProtocol) -> None:
|
|
83
|
+
"""Register a QA adapter.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
adapter: QA adapter to register
|
|
87
|
+
"""
|
|
88
|
+
adapter_name = adapter.adapter_name
|
|
89
|
+
self._adapters[adapter_name] = adapter
|
|
90
|
+
|
|
91
|
+
# Initialize adapter if not already initialized
|
|
92
|
+
await adapter.init()
|
|
93
|
+
|
|
94
|
+
def get_adapter(self, name: str) -> QAAdapterProtocol | None:
|
|
95
|
+
"""Get registered adapter by name.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
name: Adapter name
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Adapter if found, None otherwise
|
|
102
|
+
"""
|
|
103
|
+
return self._adapters.get(name)
|
|
104
|
+
|
|
105
|
+
async def run_checks(
|
|
106
|
+
self,
|
|
107
|
+
stage: str = "fast",
|
|
108
|
+
files: list[Path] | None = None,
|
|
109
|
+
) -> list[QAResult]:
|
|
110
|
+
"""Run QA checks for specified stage.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
stage: Execution stage ('fast' or 'comprehensive')
|
|
114
|
+
files: Optional list of files to check
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of QAResult objects
|
|
118
|
+
"""
|
|
119
|
+
# Get checks for this stage
|
|
120
|
+
if stage == "fast":
|
|
121
|
+
checks = self.config.fast_checks
|
|
122
|
+
elif stage == "comprehensive":
|
|
123
|
+
checks = self.config.comprehensive_checks
|
|
124
|
+
else:
|
|
125
|
+
raise ValueError(f"Invalid stage: {stage}")
|
|
126
|
+
|
|
127
|
+
# Filter enabled checks
|
|
128
|
+
checks = [c for c in checks if c.enabled]
|
|
129
|
+
|
|
130
|
+
if not checks:
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
# Sort checks: formatters first if configured
|
|
134
|
+
if self.config.run_formatters_first:
|
|
135
|
+
checks.sort(key=lambda c: (not c.is_formatter, c.check_name))
|
|
136
|
+
|
|
137
|
+
# Execute checks
|
|
138
|
+
results = await self._execute_checks(checks, files)
|
|
139
|
+
|
|
140
|
+
return results
|
|
141
|
+
|
|
142
|
+
async def run_all_checks(
|
|
143
|
+
self,
|
|
144
|
+
files: list[Path] | None = None,
|
|
145
|
+
) -> dict[str, t.Any]:
|
|
146
|
+
"""Run all registered QA checks.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
files: Optional list of files to check
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dictionary mapping adapter names to results
|
|
153
|
+
"""
|
|
154
|
+
# Run fast stage
|
|
155
|
+
fast_results = await self.run_checks(stage="fast", files=files)
|
|
156
|
+
|
|
157
|
+
# Run comprehensive stage
|
|
158
|
+
comprehensive_results = await self.run_checks(
|
|
159
|
+
stage="comprehensive", files=files
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Aggregate results
|
|
163
|
+
all_results = fast_results + comprehensive_results
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"fast_stage": fast_results,
|
|
167
|
+
"comprehensive_stage": comprehensive_results,
|
|
168
|
+
"all_results": all_results,
|
|
169
|
+
"summary": self._create_summary(all_results),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def _create_check_tasks(
|
|
173
|
+
self,
|
|
174
|
+
checks: list[QACheckConfig],
|
|
175
|
+
files: list[Path] | None,
|
|
176
|
+
) -> list[asyncio.Task]:
|
|
177
|
+
"""Create asyncio tasks for check execution."""
|
|
178
|
+
tasks = []
|
|
179
|
+
|
|
180
|
+
for check_config in checks:
|
|
181
|
+
adapter = self.get_adapter(check_config.check_name)
|
|
182
|
+
if not adapter:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Create task for this check
|
|
186
|
+
task = self._execute_single_check(adapter, check_config, files)
|
|
187
|
+
tasks.append(task)
|
|
188
|
+
|
|
189
|
+
return tasks
|
|
190
|
+
|
|
191
|
+
async def _handle_fail_fast(
|
|
192
|
+
self, tasks: list[asyncio.Task]
|
|
193
|
+
) -> list[QAResult] | None:
|
|
194
|
+
"""Handle fail-fast logic if configured."""
|
|
195
|
+
if not self.config.fail_fast or not tasks:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
# Wait for first task to complete
|
|
199
|
+
done, pending = await asyncio.wait(
|
|
200
|
+
tasks[-1:], return_when=asyncio.FIRST_COMPLETED
|
|
201
|
+
)
|
|
202
|
+
result = done.pop().result()
|
|
203
|
+
if not result.is_success:
|
|
204
|
+
# Cancel remaining tasks
|
|
205
|
+
for pending_task in pending:
|
|
206
|
+
pending_task.cancel()
|
|
207
|
+
return [result]
|
|
208
|
+
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def _filter_valid_results(self, results: list[t.Any]) -> list[QAResult]:
|
|
212
|
+
"""Filter out exceptions and convert to valid QAResult objects."""
|
|
213
|
+
valid_results = []
|
|
214
|
+
for result in results:
|
|
215
|
+
if isinstance(result, QAResult):
|
|
216
|
+
valid_results.append(result)
|
|
217
|
+
elif isinstance(result, Exception):
|
|
218
|
+
# Log error but continue
|
|
219
|
+
continue
|
|
220
|
+
return valid_results
|
|
221
|
+
|
|
222
|
+
async def _execute_checks(
|
|
223
|
+
self,
|
|
224
|
+
checks: list[QACheckConfig],
|
|
225
|
+
files: list[Path] | None,
|
|
226
|
+
) -> list[QAResult]:
|
|
227
|
+
"""Execute multiple checks in parallel.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
checks: List of check configurations
|
|
231
|
+
files: Optional files to check
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of QAResult objects
|
|
235
|
+
"""
|
|
236
|
+
tasks = self._create_check_tasks(checks, files)
|
|
237
|
+
|
|
238
|
+
# Handle fail fast logic if needed
|
|
239
|
+
fail_result = await self._handle_fail_fast(tasks)
|
|
240
|
+
if fail_result is not None:
|
|
241
|
+
return fail_result
|
|
242
|
+
|
|
243
|
+
# Execute all tasks
|
|
244
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
245
|
+
|
|
246
|
+
# Filter out exceptions and convert to QAResult
|
|
247
|
+
return self._filter_valid_results(results)
|
|
248
|
+
|
|
249
|
+
async def _execute_single_check(
|
|
250
|
+
self,
|
|
251
|
+
adapter: QAAdapterProtocol,
|
|
252
|
+
config: QACheckConfig,
|
|
253
|
+
files: list[Path] | None,
|
|
254
|
+
) -> QAResult:
|
|
255
|
+
"""Execute a single check with caching and semaphore control.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
adapter: QA adapter to execute
|
|
259
|
+
config: Check configuration
|
|
260
|
+
files: Optional files to check
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
QAResult
|
|
264
|
+
"""
|
|
265
|
+
# Generate cache key
|
|
266
|
+
cache_key = self._generate_cache_key(adapter, config, files)
|
|
267
|
+
|
|
268
|
+
# Check cache if enabled
|
|
269
|
+
if self.config.enable_caching:
|
|
270
|
+
cached_result = self._get_cached_result(cache_key)
|
|
271
|
+
if cached_result:
|
|
272
|
+
return cached_result
|
|
273
|
+
|
|
274
|
+
# Acquire semaphore for parallel execution control
|
|
275
|
+
async with self._semaphore:
|
|
276
|
+
try:
|
|
277
|
+
# Execute check
|
|
278
|
+
result = await adapter.check(files=files, config=config)
|
|
279
|
+
|
|
280
|
+
# Cache result if enabled
|
|
281
|
+
if self.config.enable_caching:
|
|
282
|
+
self._cache_result(cache_key, result)
|
|
283
|
+
|
|
284
|
+
# Retry on failure if configured
|
|
285
|
+
if not result.is_success and config.retry_on_failure:
|
|
286
|
+
result = await adapter.check(files=files, config=config)
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
# Return error result
|
|
292
|
+
return QAResult(
|
|
293
|
+
check_id=config.check_id,
|
|
294
|
+
check_name=adapter.adapter_name,
|
|
295
|
+
check_type=config.check_type,
|
|
296
|
+
status=QAResultStatus.ERROR,
|
|
297
|
+
message=f"Check failed: {e}",
|
|
298
|
+
details=str(e),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _generate_cache_key(
|
|
302
|
+
self,
|
|
303
|
+
adapter: QAAdapterProtocol,
|
|
304
|
+
config: QACheckConfig,
|
|
305
|
+
files: list[Path] | None,
|
|
306
|
+
) -> str:
|
|
307
|
+
"""Generate cache key for check execution.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
adapter: QA adapter
|
|
311
|
+
config: Check configuration
|
|
312
|
+
files: Files to check
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Cache key string
|
|
316
|
+
"""
|
|
317
|
+
key_parts = [
|
|
318
|
+
adapter.adapter_name,
|
|
319
|
+
str(config.check_id),
|
|
320
|
+
str(sorted([str(f) for f in files]) if files else "all"),
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
key_string = "|".join(key_parts)
|
|
324
|
+
return hashlib.sha256(key_string.encode()).hexdigest()[:16]
|
|
325
|
+
|
|
326
|
+
def _get_cached_result(self, cache_key: str) -> QAResult | None:
|
|
327
|
+
"""Get cached result if valid.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
cache_key: Cache key
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Cached QAResult or None
|
|
334
|
+
"""
|
|
335
|
+
if cache_key not in self._cache:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
result, timestamp = self._cache[cache_key]
|
|
339
|
+
|
|
340
|
+
# Check if cache is still valid (1 hour TTL)
|
|
341
|
+
if datetime.now() - timestamp > timedelta(hours=1):
|
|
342
|
+
del self._cache[cache_key]
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
return result
|
|
346
|
+
|
|
347
|
+
def _cache_result(self, cache_key: str, result: QAResult) -> None:
|
|
348
|
+
"""Cache check result.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
cache_key: Cache key
|
|
352
|
+
result: QAResult to cache
|
|
353
|
+
"""
|
|
354
|
+
self._cache[cache_key] = (result, datetime.now())
|
|
355
|
+
|
|
356
|
+
def _create_summary(self, results: list[QAResult]) -> dict[str, t.Any]:
|
|
357
|
+
"""Create summary statistics from results.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
results: List of QAResult objects
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Summary dictionary
|
|
364
|
+
"""
|
|
365
|
+
total_checks = len(results)
|
|
366
|
+
success_count = sum(1 for r in results if r.is_success)
|
|
367
|
+
failure_count = sum(1 for r in results if r.status == QAResultStatus.FAILURE)
|
|
368
|
+
error_count = sum(1 for r in results if r.status == QAResultStatus.ERROR)
|
|
369
|
+
warning_count = sum(1 for r in results if r.status == QAResultStatus.WARNING)
|
|
370
|
+
|
|
371
|
+
total_issues = sum(r.issues_found for r in results)
|
|
372
|
+
total_fixed = sum(r.issues_fixed for r in results)
|
|
373
|
+
total_execution_time = sum(r.execution_time_ms for r in results)
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
"total_checks": total_checks,
|
|
377
|
+
"success": success_count,
|
|
378
|
+
"failures": failure_count,
|
|
379
|
+
"errors": error_count,
|
|
380
|
+
"warnings": warning_count,
|
|
381
|
+
"total_issues_found": total_issues,
|
|
382
|
+
"total_issues_fixed": total_fixed,
|
|
383
|
+
"total_execution_time_ms": total_execution_time,
|
|
384
|
+
"pass_rate": success_count / total_checks if total_checks > 0 else 0.0,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@classmethod
|
|
388
|
+
async def from_yaml_config(
|
|
389
|
+
cls,
|
|
390
|
+
config_path: Path,
|
|
391
|
+
project_root: Path | None = None,
|
|
392
|
+
) -> QAOrchestrator:
|
|
393
|
+
"""Create orchestrator from YAML configuration.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
config_path: Path to YAML configuration file
|
|
397
|
+
project_root: Optional project root override
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Configured QAOrchestrator instance
|
|
401
|
+
|
|
402
|
+
Example YAML:
|
|
403
|
+
```yaml
|
|
404
|
+
project_root: .
|
|
405
|
+
max_parallel_checks: 4
|
|
406
|
+
enable_caching: true
|
|
407
|
+
fail_fast: false
|
|
408
|
+
run_formatters_first: true
|
|
409
|
+
|
|
410
|
+
checks:
|
|
411
|
+
- check_name: ruff-lint
|
|
412
|
+
check_type: lint
|
|
413
|
+
enabled: true
|
|
414
|
+
stage: fast
|
|
415
|
+
settings:
|
|
416
|
+
mode: check
|
|
417
|
+
fix_enabled: false
|
|
418
|
+
|
|
419
|
+
- check_name: ruff-format
|
|
420
|
+
check_type: format
|
|
421
|
+
enabled: true
|
|
422
|
+
stage: fast
|
|
423
|
+
settings:
|
|
424
|
+
mode: format
|
|
425
|
+
fix_enabled: false
|
|
426
|
+
|
|
427
|
+
- check_name: bandit
|
|
428
|
+
check_type: security
|
|
429
|
+
enabled: true
|
|
430
|
+
stage: comprehensive
|
|
431
|
+
```
|
|
432
|
+
"""
|
|
433
|
+
if not config_path.exists():
|
|
434
|
+
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
435
|
+
|
|
436
|
+
# Load YAML configuration
|
|
437
|
+
with config_path.open() as f:
|
|
438
|
+
config_data = yaml.safe_load(f)
|
|
439
|
+
|
|
440
|
+
# Create orchestrator config
|
|
441
|
+
if project_root is None:
|
|
442
|
+
project_root = Path(config_data.get("project_root", "."))
|
|
443
|
+
|
|
444
|
+
config = QAOrchestratorConfig(
|
|
445
|
+
project_root=project_root,
|
|
446
|
+
max_parallel_checks=config_data.get("max_parallel_checks", 4),
|
|
447
|
+
enable_caching=config_data.get("enable_caching", True),
|
|
448
|
+
fail_fast=config_data.get("fail_fast", False),
|
|
449
|
+
run_formatters_first=config_data.get("run_formatters_first", True),
|
|
450
|
+
enable_incremental=config_data.get("enable_incremental", True),
|
|
451
|
+
verbose=config_data.get("verbose", False),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Create orchestrator
|
|
455
|
+
orchestrator = cls(config)
|
|
456
|
+
|
|
457
|
+
# Load and register adapters based on configuration
|
|
458
|
+
# This would be implemented to dynamically load adapters
|
|
459
|
+
# based on the YAML configuration
|
|
460
|
+
|
|
461
|
+
return orchestrator
|
|
462
|
+
|
|
463
|
+
async def health_check(self) -> dict[str, t.Any]:
|
|
464
|
+
"""Check health of orchestrator and all adapters.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
Health status dictionary
|
|
468
|
+
"""
|
|
469
|
+
adapter_health = {}
|
|
470
|
+
|
|
471
|
+
for name, adapter in self._adapters.items():
|
|
472
|
+
try:
|
|
473
|
+
health = await adapter.health_check()
|
|
474
|
+
adapter_health[name] = health
|
|
475
|
+
except Exception as e:
|
|
476
|
+
adapter_health[name] = {
|
|
477
|
+
"status": "error",
|
|
478
|
+
"error": str(e),
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
"orchestrator_status": "healthy",
|
|
483
|
+
"registered_adapters": len(self._adapters),
|
|
484
|
+
"cache_entries": len(self._cache),
|
|
485
|
+
"adapters": adapter_health,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# Register orchestrator with ACB dependency injection
|
|
490
|
+
with contextlib.suppress(Exception):
|
|
491
|
+
depends.set(QAOrchestrator)
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
1
3
|
import subprocess
|
|
2
4
|
import typing as t
|
|
3
5
|
from dataclasses import asdict, dataclass
|
|
4
6
|
from datetime import datetime
|
|
5
7
|
|
|
8
|
+
from acb.depends import depends
|
|
9
|
+
|
|
10
|
+
from crackerjack.data.models import QualityBaselineRecord
|
|
11
|
+
from crackerjack.data.repository import QualityBaselineRepository
|
|
12
|
+
from crackerjack.models.protocols import QualityBaselineProtocol
|
|
6
13
|
from crackerjack.services.cache import CrackerjackCache
|
|
7
14
|
|
|
8
15
|
|
|
@@ -35,11 +42,23 @@ class QualityMetrics:
|
|
|
35
42
|
return cls(**data)
|
|
36
43
|
|
|
37
44
|
|
|
38
|
-
class QualityBaselineService:
|
|
45
|
+
class QualityBaselineService(QualityBaselineProtocol):
|
|
39
46
|
"""Service for tracking and persisting quality baselines across sessions."""
|
|
40
47
|
|
|
41
|
-
def __init__(
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
cache: CrackerjackCache | None = None,
|
|
51
|
+
repository: QualityBaselineRepository | None = None,
|
|
52
|
+
) -> None:
|
|
42
53
|
self.cache = cache or CrackerjackCache()
|
|
54
|
+
self._logger = logging.getLogger(__name__)
|
|
55
|
+
if repository is not None:
|
|
56
|
+
self._repository: QualityBaselineRepository | None = repository
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
self._repository = depends.get_sync(QualityBaselineRepository)
|
|
60
|
+
except Exception:
|
|
61
|
+
self._repository = None
|
|
43
62
|
|
|
44
63
|
def get_current_git_hash(self) -> str | None:
|
|
45
64
|
"""Get current git commit hash."""
|
|
@@ -92,6 +111,31 @@ class QualityBaselineService:
|
|
|
92
111
|
security_issues: int = 0,
|
|
93
112
|
type_errors: int = 0,
|
|
94
113
|
linting_issues: int = 0,
|
|
114
|
+
) -> QualityMetrics | None:
|
|
115
|
+
"""Synchronous wrapper for asynchronous baseline recording."""
|
|
116
|
+
return self._run_async(
|
|
117
|
+
self.arecord_baseline(
|
|
118
|
+
coverage_percent=coverage_percent,
|
|
119
|
+
test_count=test_count,
|
|
120
|
+
test_pass_rate=test_pass_rate,
|
|
121
|
+
hook_failures=hook_failures,
|
|
122
|
+
complexity_violations=complexity_violations,
|
|
123
|
+
security_issues=security_issues,
|
|
124
|
+
type_errors=type_errors,
|
|
125
|
+
linting_issues=linting_issues,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def arecord_baseline(
|
|
130
|
+
self,
|
|
131
|
+
coverage_percent: float,
|
|
132
|
+
test_count: int,
|
|
133
|
+
test_pass_rate: float,
|
|
134
|
+
hook_failures: int = 0,
|
|
135
|
+
complexity_violations: int = 0,
|
|
136
|
+
security_issues: int = 0,
|
|
137
|
+
type_errors: int = 0,
|
|
138
|
+
linting_issues: int = 0,
|
|
95
139
|
) -> QualityMetrics | None:
|
|
96
140
|
"""Record quality baseline for current commit."""
|
|
97
141
|
git_hash = self.get_current_git_hash()
|
|
@@ -124,9 +168,17 @@ class QualityBaselineService:
|
|
|
124
168
|
|
|
125
169
|
# Store in cache for persistence across sessions
|
|
126
170
|
self.cache.set_quality_baseline(git_hash, metrics.to_dict())
|
|
171
|
+
await self._persist_metrics(metrics)
|
|
127
172
|
return metrics
|
|
128
173
|
|
|
129
174
|
def get_baseline(self, git_hash: str | None = None) -> QualityMetrics | None:
|
|
175
|
+
"""Synchronous wrapper around asynchronous baseline retrieval."""
|
|
176
|
+
return self._run_async(self.aget_baseline(git_hash=git_hash))
|
|
177
|
+
|
|
178
|
+
async def aget_baseline(
|
|
179
|
+
self,
|
|
180
|
+
git_hash: str | None = None,
|
|
181
|
+
) -> QualityMetrics | None:
|
|
130
182
|
"""Get quality baseline for specific commit (or current commit)."""
|
|
131
183
|
if not git_hash:
|
|
132
184
|
git_hash = self.get_current_git_hash()
|
|
@@ -134,6 +186,13 @@ class QualityBaselineService:
|
|
|
134
186
|
if not git_hash:
|
|
135
187
|
return None
|
|
136
188
|
|
|
189
|
+
if self._repository:
|
|
190
|
+
record = await self._repository.get_by_git_hash(git_hash)
|
|
191
|
+
if record:
|
|
192
|
+
metrics = self._record_to_metrics(record)
|
|
193
|
+
self.cache.set_quality_baseline(git_hash, metrics.to_dict())
|
|
194
|
+
return metrics
|
|
195
|
+
|
|
137
196
|
baseline_data = self.cache.get_quality_baseline(git_hash)
|
|
138
197
|
if baseline_data:
|
|
139
198
|
return QualityMetrics.from_dict(baseline_data)
|
|
@@ -212,7 +271,15 @@ class QualityBaselineService:
|
|
|
212
271
|
return regressions
|
|
213
272
|
|
|
214
273
|
def get_recent_baselines(self, limit: int = 10) -> list[QualityMetrics]:
|
|
274
|
+
"""Synchronous wrapper around asynchronous baseline listing."""
|
|
275
|
+
return self._run_async(self.aget_recent_baselines(limit=limit))
|
|
276
|
+
|
|
277
|
+
async def aget_recent_baselines(self, limit: int = 10) -> list[QualityMetrics]:
|
|
215
278
|
"""Get recent baselines (requires git log parsing since cache is keyed by hash)."""
|
|
279
|
+
if self._repository:
|
|
280
|
+
records = await self._repository.list_recent(limit=limit)
|
|
281
|
+
return [self._record_to_metrics(record) for record in records]
|
|
282
|
+
|
|
216
283
|
try:
|
|
217
284
|
result = subprocess.run(
|
|
218
285
|
["git", "log", "--oneline", "-n", str(limit), "--format=%H"],
|
|
@@ -232,3 +299,97 @@ class QualityBaselineService:
|
|
|
232
299
|
|
|
233
300
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
234
301
|
return []
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------ #
|
|
304
|
+
# Internal helpers
|
|
305
|
+
# ------------------------------------------------------------------ #
|
|
306
|
+
async def _persist_metrics(self, metrics: QualityMetrics) -> None:
|
|
307
|
+
if not self._repository:
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
await self._repository.upsert(
|
|
312
|
+
{
|
|
313
|
+
"git_hash": metrics.git_hash,
|
|
314
|
+
"recorded_at": metrics.timestamp,
|
|
315
|
+
"coverage_percent": metrics.coverage_percent,
|
|
316
|
+
"test_count": metrics.test_count,
|
|
317
|
+
"test_pass_rate": metrics.test_pass_rate,
|
|
318
|
+
"hook_failures": metrics.hook_failures,
|
|
319
|
+
"complexity_violations": metrics.complexity_violations,
|
|
320
|
+
"security_issues": metrics.security_issues,
|
|
321
|
+
"type_errors": metrics.type_errors,
|
|
322
|
+
"linting_issues": metrics.linting_issues,
|
|
323
|
+
"quality_score": metrics.quality_score,
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
327
|
+
self._logger.debug(
|
|
328
|
+
"Failed to persist quality baseline record",
|
|
329
|
+
exc_info=exc,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def _record_to_metrics(self, record: QualityBaselineRecord) -> QualityMetrics:
|
|
333
|
+
return QualityMetrics(
|
|
334
|
+
git_hash=record.git_hash,
|
|
335
|
+
timestamp=record.recorded_at,
|
|
336
|
+
coverage_percent=record.coverage_percent,
|
|
337
|
+
test_count=record.test_count,
|
|
338
|
+
test_pass_rate=record.test_pass_rate,
|
|
339
|
+
hook_failures=record.hook_failures,
|
|
340
|
+
complexity_violations=record.complexity_violations,
|
|
341
|
+
security_issues=record.security_issues,
|
|
342
|
+
type_errors=record.type_errors,
|
|
343
|
+
linting_issues=record.linting_issues,
|
|
344
|
+
quality_score=record.quality_score,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def _run_async(self, coro: t.Awaitable[t.Any]) -> t.Any:
|
|
348
|
+
try:
|
|
349
|
+
asyncio.get_running_loop()
|
|
350
|
+
except RuntimeError:
|
|
351
|
+
return asyncio.run(coro)
|
|
352
|
+
msg = (
|
|
353
|
+
"QualityBaselineService synchronous method called while an event loop is "
|
|
354
|
+
"running. Use the corresponding async method instead."
|
|
355
|
+
)
|
|
356
|
+
raise RuntimeError(msg)
|
|
357
|
+
|
|
358
|
+
# Protocol methods
|
|
359
|
+
def get_current_baseline(self) -> dict[str, t.Any]:
|
|
360
|
+
"""Protocol method for getting baseline metrics."""
|
|
361
|
+
baseline = self.get_baseline() # Call the existing method
|
|
362
|
+
if baseline:
|
|
363
|
+
return baseline.to_dict()
|
|
364
|
+
return {}
|
|
365
|
+
|
|
366
|
+
def update_baseline(self, metrics: dict[str, t.Any]) -> bool:
|
|
367
|
+
"""Protocol method for updating baseline metrics."""
|
|
368
|
+
try:
|
|
369
|
+
# Extract required values from metrics dict
|
|
370
|
+
coverage_percent = metrics.get("coverage_percent", 0.0)
|
|
371
|
+
test_count = metrics.get("test_count", 0)
|
|
372
|
+
test_pass_rate = metrics.get("test_pass_rate", 0.0)
|
|
373
|
+
hook_failures = metrics.get("hook_failures", 0)
|
|
374
|
+
complexity_violations = metrics.get("complexity_violations", 0)
|
|
375
|
+
security_issues = metrics.get("security_issues", 0)
|
|
376
|
+
type_errors = metrics.get("type_errors", 0)
|
|
377
|
+
linting_issues = metrics.get("linting_issues", 0)
|
|
378
|
+
|
|
379
|
+
result = self.record_baseline(
|
|
380
|
+
coverage_percent=coverage_percent,
|
|
381
|
+
test_count=test_count,
|
|
382
|
+
test_pass_rate=test_pass_rate,
|
|
383
|
+
hook_failures=hook_failures,
|
|
384
|
+
complexity_violations=complexity_violations,
|
|
385
|
+
security_issues=security_issues,
|
|
386
|
+
type_errors=type_errors,
|
|
387
|
+
linting_issues=linting_issues,
|
|
388
|
+
)
|
|
389
|
+
return result is not None
|
|
390
|
+
except Exception:
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
def compare(self, current: dict[str, t.Any]) -> dict[str, t.Any]:
|
|
394
|
+
"""Protocol method for comparing current metrics against baseline."""
|
|
395
|
+
return self.compare_with_baseline(current)
|