crackerjack 0.18.2__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 +96 -2
- crackerjack/__main__.py +637 -138
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +39 -0
- 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/lsp/_base.py +194 -0
- crackerjack/adapters/lsp/_client.py +358 -0
- crackerjack/adapters/lsp/_manager.py +193 -0
- crackerjack/adapters/lsp/skylos.py +283 -0
- crackerjack/adapters/lsp/zuban.py +557 -0
- 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 +66 -0
- crackerjack/agents/architect_agent.py +238 -0
- crackerjack/agents/base.py +167 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +600 -0
- crackerjack/agents/documentation_agent.py +520 -0
- crackerjack/agents/dry_agent.py +585 -0
- 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 +230 -0
- 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/import_optimization_agent.py +1181 -0
- crackerjack/agents/performance_agent.py +325 -0
- crackerjack/agents/performance_helpers.py +205 -0
- crackerjack/agents/proactive_agent.py +55 -0
- crackerjack/agents/refactoring_agent.py +511 -0
- crackerjack/agents/refactoring_helpers.py +247 -0
- crackerjack/agents/security_agent.py +793 -0
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +570 -0
- crackerjack/agents/test_specialist_agent.py +526 -0
- crackerjack/agents/tracker.py +110 -0
- crackerjack/api.py +647 -0
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/cache_handlers.py +209 -0
- crackerjack/cli/cache_handlers_enhanced.py +680 -0
- crackerjack/cli/facade.py +162 -0
- 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 +700 -0
- crackerjack/cli/interactive.py +488 -0
- crackerjack/cli/options.py +1216 -0
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/utils.py +19 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +1307 -0
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +275 -0
- crackerjack/config/global_lock_config.py +207 -0
- crackerjack/config/hooks.py +390 -0
- 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/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +738 -0
- crackerjack/core/autofix_coordinator.py +282 -0
- crackerjack/core/container.py +105 -0
- crackerjack/core/enhanced_container.py +583 -0
- crackerjack/core/file_lifecycle.py +472 -0
- crackerjack/core/performance.py +244 -0
- crackerjack/core/performance_monitor.py +357 -0
- crackerjack/core/phase_coordinator.py +1227 -0
- crackerjack/core/proactive_workflow.py +267 -0
- crackerjack/core/resource_manager.py +425 -0
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +601 -0
- crackerjack/core/session_coordinator.py +239 -0
- crackerjack/core/timeout_manager.py +563 -0
- crackerjack/core/websocket_lifecycle.py +410 -0
- 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 +2243 -0
- 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/INDEX.md +11 -0
- crackerjack/docs/README.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +767 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +1065 -0
- crackerjack/dynamic_config.py +678 -0
- crackerjack/errors.py +378 -0
- 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 +13 -0
- crackerjack/executors/async_hook_executor.py +938 -0
- crackerjack/executors/cached_hook_executor.py +316 -0
- crackerjack/executors/hook_executor.py +1295 -0
- crackerjack/executors/hook_lock_manager.py +708 -0
- crackerjack/executors/individual_hook_executor.py +739 -0
- crackerjack/executors/lsp_aware_hook_executor.py +349 -0
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +433 -0
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +93 -0
- crackerjack/intelligence/README.md +557 -0
- crackerjack/intelligence/__init__.py +37 -0
- crackerjack/intelligence/adaptive_learning.py +693 -0
- crackerjack/intelligence/agent_orchestrator.py +485 -0
- crackerjack/intelligence/agent_registry.py +377 -0
- crackerjack/intelligence/agent_selector.py +439 -0
- crackerjack/intelligence/integration.py +250 -0
- crackerjack/interactive.py +719 -0
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +585 -0
- crackerjack/managers/publish_manager.py +631 -0
- crackerjack/managers/test_command_builder.py +391 -0
- crackerjack/managers/test_executor.py +474 -0
- crackerjack/managers/test_manager.py +1357 -0
- crackerjack/managers/test_progress.py +187 -0
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +352 -0
- crackerjack/mcp/client_runner.py +121 -0
- crackerjack/mcp/context.py +802 -0
- crackerjack/mcp/dashboard.py +657 -0
- crackerjack/mcp/enhanced_progress_monitor.py +493 -0
- crackerjack/mcp/file_monitor.py +394 -0
- crackerjack/mcp/progress_components.py +607 -0
- crackerjack/mcp/progress_monitor.py +1016 -0
- crackerjack/mcp/rate_limiter.py +336 -0
- crackerjack/mcp/server.py +24 -0
- crackerjack/mcp/server_core.py +526 -0
- crackerjack/mcp/service_watchdog.py +505 -0
- crackerjack/mcp/state.py +407 -0
- crackerjack/mcp/task_manager.py +259 -0
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +19 -0
- crackerjack/mcp/tools/core_tools.py +469 -0
- crackerjack/mcp/tools/error_analyzer.py +283 -0
- crackerjack/mcp/tools/execution_tools.py +384 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +46 -0
- crackerjack/mcp/tools/intelligence_tools.py +264 -0
- crackerjack/mcp/tools/monitoring_tools.py +628 -0
- crackerjack/mcp/tools/proactive_tools.py +367 -0
- crackerjack/mcp/tools/progress_tools.py +222 -0
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +358 -0
- crackerjack/mcp/tools/workflow_executor.py +699 -0
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +54 -0
- crackerjack/mcp/websocket/endpoints.py +492 -0
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +406 -0
- 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 +21 -0
- crackerjack/mcp/websocket/server.py +174 -0
- crackerjack/mcp/websocket/websocket_handler.py +276 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +40 -0
- crackerjack/models/config.py +730 -0
- crackerjack/models/config_adapter.py +265 -0
- crackerjack/models/protocols.py +1535 -0
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/resource_protocols.py +299 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +173 -0
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +405 -0
- crackerjack/monitoring/metrics_collector.py +427 -0
- crackerjack/monitoring/regression_prevention.py +580 -0
- crackerjack/monitoring/websocket_server.py +406 -0
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +894 -0
- 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 +180 -0
- crackerjack/orchestration/execution_strategies.py +361 -0
- 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 +647 -0
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +254 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +264 -0
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/security/__init__.py +0 -0
- crackerjack/security/audit.py +197 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +9 -0
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/ai/contextual_ai_assistant.py +542 -0
- 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/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +617 -0
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +530 -0
- crackerjack/services/cache.py +369 -0
- crackerjack/services/changelog_automation.py +399 -0
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +132 -0
- crackerjack/services/config_merge.py +546 -0
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +493 -0
- crackerjack/services/coverage_badge_service.py +173 -0
- crackerjack/services/coverage_ratchet.py +381 -0
- crackerjack/services/debug.py +733 -0
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +622 -0
- crackerjack/services/documentation_generator.py +493 -0
- crackerjack/services/documentation_service.py +704 -0
- crackerjack/services/enhanced_filesystem.py +497 -0
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +149 -0
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +381 -0
- crackerjack/services/git.py +422 -0
- crackerjack/services/health_metrics.py +615 -0
- crackerjack/services/heatmap_generator.py +744 -0
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +823 -0
- crackerjack/services/input_validator.py +668 -0
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +289 -0
- crackerjack/services/logging.py +228 -0
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +414 -0
- crackerjack/services/metrics.py +587 -0
- 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/monitoring/performance_benchmarks.py +410 -0
- crackerjack/services/monitoring/performance_cache.py +388 -0
- crackerjack/services/monitoring/performance_monitor.py +569 -0
- crackerjack/services/parallel_executor.py +527 -0
- crackerjack/services/pattern_cache.py +333 -0
- crackerjack/services/pattern_detector.py +478 -0
- 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 +523 -0
- 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/quality_baseline.py +395 -0
- crackerjack/services/quality/quality_baseline_enhanced.py +649 -0
- crackerjack/services/quality/quality_intelligence.py +949 -0
- crackerjack/services/regex_patterns.py +58 -0
- crackerjack/services/regex_utils.py +483 -0
- crackerjack/services/secure_path_utils.py +524 -0
- crackerjack/services/secure_status_formatter.py +450 -0
- crackerjack/services/secure_subprocess.py +635 -0
- crackerjack/services/security.py +239 -0
- crackerjack/services/security_logger.py +495 -0
- crackerjack/services/server_manager.py +411 -0
- crackerjack/services/smart_scheduling.py +167 -0
- crackerjack/services/status_authentication.py +460 -0
- crackerjack/services/status_security_manager.py +315 -0
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +441 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +43 -0
- crackerjack/services/unified_config.py +115 -0
- crackerjack/services/validation_rate_limiter.py +220 -0
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +461 -0
- crackerjack/services/version_checker.py +223 -0
- crackerjack/services/websocket_resource_limiter.py +438 -0
- crackerjack/services/zuban_lsp_service.py +391 -0
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/__init__.py +59 -0
- crackerjack/slash_commands/init.md +112 -0
- crackerjack/slash_commands/run.md +197 -0
- crackerjack/slash_commands/status.md +127 -0
- 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_input_validator_patterns.py +236 -0
- crackerjack/tools/validate_regex_patterns.py +188 -0
- crackerjack/ui/README.md +11 -0
- crackerjack/ui/__init__.py +1 -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.45.2.dist-info/METADATA +1678 -0
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack-0.45.2.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -14
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/.pre-commit-config.yaml +0 -91
- crackerjack/.pytest_cache/.gitignore +0 -2
- crackerjack/.pytest_cache/CACHEDIR.TAG +0 -4
- crackerjack/.pytest_cache/README.md +0 -8
- crackerjack/.pytest_cache/v/cache/nodeids +0 -1
- crackerjack/.pytest_cache/v/cache/stepwise +0 -1
- crackerjack/.ruff_cache/.gitignore +0 -1
- crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
- crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
- crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
- crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
- crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
- crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
- crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
- crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
- crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
- crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
- crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
- crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
- crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
- crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
- crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
- crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
- crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
- crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
- crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
- crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
- crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
- crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
- crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
- crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
- crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
- crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
- crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
- crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
- crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
- crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
- crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
- crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
- crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
- crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
- crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
- crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
- crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
- crackerjack/.ruff_cache/CACHEDIR.TAG +0 -1
- crackerjack/crackerjack.py +0 -855
- crackerjack/pyproject.toml +0 -214
- crackerjack-0.18.2.dist-info/METADATA +0 -420
- crackerjack-0.18.2.dist-info/RECORD +0 -59
- crackerjack-0.18.2.dist-info/entry_points.txt +0 -4
- {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
import typing as t
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from acb.console import Console
|
|
9
|
+
from acb.depends import Inject, depends
|
|
10
|
+
from acb.logger import Logger
|
|
11
|
+
|
|
12
|
+
from crackerjack.config import get_console_width
|
|
13
|
+
from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
|
|
14
|
+
from crackerjack.models.protocols import HookLockManagerProtocol
|
|
15
|
+
from crackerjack.models.task import HookResult
|
|
16
|
+
from crackerjack.services.logging import LoggingContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AsyncHookExecutionResult:
|
|
21
|
+
strategy_name: str
|
|
22
|
+
results: list[HookResult]
|
|
23
|
+
total_duration: float
|
|
24
|
+
success: bool
|
|
25
|
+
concurrent_execution: bool = True
|
|
26
|
+
cache_hits: int = 0
|
|
27
|
+
cache_misses: int = 0
|
|
28
|
+
performance_gain: float = 0.0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def failed_count(self) -> int:
|
|
32
|
+
return sum(1 for r in self.results if r.status == "failed")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def passed_count(self) -> int:
|
|
36
|
+
return sum(1 for r in self.results if r.status == "passed")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def cache_hit_rate(self) -> float:
|
|
40
|
+
total_requests = self.cache_hits + self.cache_misses
|
|
41
|
+
return (self.cache_hits / total_requests * 100) if total_requests > 0 else 0.0
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def performance_summary(self) -> dict[str, t.Any]:
|
|
45
|
+
return {
|
|
46
|
+
"total_hooks": len(self.results),
|
|
47
|
+
"passed": self.passed_count,
|
|
48
|
+
"failed": self.failed_count,
|
|
49
|
+
"duration_seconds": round(self.total_duration, 2),
|
|
50
|
+
"concurrent": self.concurrent_execution,
|
|
51
|
+
"cache_hits": self.cache_hits,
|
|
52
|
+
"cache_misses": self.cache_misses,
|
|
53
|
+
"cache_hit_rate_percent": round(self.cache_hit_rate, 1),
|
|
54
|
+
"performance_gain_percent": round(self.performance_gain, 1),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AsyncHookExecutor:
|
|
59
|
+
@depends.inject
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
logger: Inject[Logger],
|
|
63
|
+
console: Console,
|
|
64
|
+
pkg_path: Path,
|
|
65
|
+
max_concurrent: int = 4,
|
|
66
|
+
timeout: int = 300,
|
|
67
|
+
quiet: bool = False,
|
|
68
|
+
hook_lock_manager: HookLockManagerProtocol | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
self.console = console
|
|
71
|
+
self.pkg_path = pkg_path
|
|
72
|
+
self.max_concurrent = max_concurrent
|
|
73
|
+
self.timeout = timeout
|
|
74
|
+
self.quiet = quiet
|
|
75
|
+
self.logger = logger
|
|
76
|
+
|
|
77
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
78
|
+
self._running_processes: set = set() # Track running subprocesses
|
|
79
|
+
self._last_stdout: bytes | None = None
|
|
80
|
+
self._last_stderr: bytes | None = None
|
|
81
|
+
|
|
82
|
+
if hook_lock_manager is None:
|
|
83
|
+
from crackerjack.executors.hook_lock_manager import (
|
|
84
|
+
hook_lock_manager as default_manager,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.hook_lock_manager: HookLockManagerProtocol = t.cast(
|
|
88
|
+
HookLockManagerProtocol, default_manager
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
self.hook_lock_manager = hook_lock_manager
|
|
92
|
+
|
|
93
|
+
async def execute_strategy(
|
|
94
|
+
self,
|
|
95
|
+
strategy: HookStrategy,
|
|
96
|
+
) -> AsyncHookExecutionResult:
|
|
97
|
+
with LoggingContext(
|
|
98
|
+
"async_hook_strategy",
|
|
99
|
+
strategy_name=strategy.name,
|
|
100
|
+
hook_count=len(strategy.hooks),
|
|
101
|
+
):
|
|
102
|
+
start_time = time.time()
|
|
103
|
+
self.logger.info(
|
|
104
|
+
"Starting async hook strategy execution",
|
|
105
|
+
strategy=strategy.name,
|
|
106
|
+
hooks=len(strategy.hooks),
|
|
107
|
+
parallel=strategy.parallel,
|
|
108
|
+
max_workers=getattr(strategy, "max_workers", self.max_concurrent),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Header is displayed by PhaseCoordinator; suppress here to avoid duplicates
|
|
112
|
+
|
|
113
|
+
estimated_sequential = sum(
|
|
114
|
+
getattr(hook, "timeout", 30) for hook in strategy.hooks
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if strategy.parallel and len(strategy.hooks) > 1:
|
|
118
|
+
results = await self._execute_parallel(strategy)
|
|
119
|
+
else:
|
|
120
|
+
results = await self._execute_sequential(strategy)
|
|
121
|
+
|
|
122
|
+
if strategy.retry_policy != RetryPolicy.NONE:
|
|
123
|
+
results = await self._handle_retries(strategy, results)
|
|
124
|
+
|
|
125
|
+
total_duration = time.time() - start_time
|
|
126
|
+
success = all(r.status == "passed" for r in results)
|
|
127
|
+
performance_gain = max(
|
|
128
|
+
0,
|
|
129
|
+
((estimated_sequential - total_duration) / estimated_sequential) * 100,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self.logger.info(
|
|
133
|
+
"Async hook strategy completed",
|
|
134
|
+
strategy=strategy.name,
|
|
135
|
+
success=success,
|
|
136
|
+
duration_seconds=round(total_duration, 2),
|
|
137
|
+
performance_gain_percent=round(performance_gain, 1),
|
|
138
|
+
passed=sum(1 for r in results if r.status == "passed"),
|
|
139
|
+
failed=sum(1 for r in results if r.status == "failed"),
|
|
140
|
+
errors=sum(1 for r in results if r.status in ("timeout", "error")),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not self.quiet:
|
|
144
|
+
self._print_summary(strategy, results, success, performance_gain)
|
|
145
|
+
|
|
146
|
+
return AsyncHookExecutionResult(
|
|
147
|
+
strategy_name=strategy.name,
|
|
148
|
+
results=results,
|
|
149
|
+
total_duration=total_duration,
|
|
150
|
+
success=success,
|
|
151
|
+
performance_gain=performance_gain,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def get_lock_statistics(self) -> dict[str, t.Any]:
|
|
155
|
+
return self.hook_lock_manager.get_lock_stats()
|
|
156
|
+
|
|
157
|
+
def get_comprehensive_status(self) -> dict[str, t.Any]:
|
|
158
|
+
return {
|
|
159
|
+
"executor_config": {
|
|
160
|
+
"max_concurrent": self.max_concurrent,
|
|
161
|
+
"timeout": self.timeout,
|
|
162
|
+
"quiet": self.quiet,
|
|
163
|
+
},
|
|
164
|
+
"lock_manager_status": self.hook_lock_manager.get_lock_stats(),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def _print_strategy_header(self, strategy: HookStrategy) -> None:
|
|
168
|
+
# Intentionally no-op: PhaseCoordinator controls stage headers
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
async def _execute_sequential(self, strategy: HookStrategy) -> list[HookResult]:
|
|
172
|
+
results: list[HookResult] = []
|
|
173
|
+
for hook in strategy.hooks:
|
|
174
|
+
result = await self._execute_single_hook(hook)
|
|
175
|
+
results.append(result)
|
|
176
|
+
self._display_hook_result(result)
|
|
177
|
+
return results
|
|
178
|
+
|
|
179
|
+
async def _execute_parallel(self, strategy: HookStrategy) -> list[HookResult]:
|
|
180
|
+
results: list[HookResult] = []
|
|
181
|
+
|
|
182
|
+
formatting_hooks = [
|
|
183
|
+
h for h in strategy.hooks if getattr(h, "is_formatting", False)
|
|
184
|
+
]
|
|
185
|
+
other_hooks = [
|
|
186
|
+
h for h in strategy.hooks if not getattr(h, "is_formatting", False)
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
for hook in formatting_hooks:
|
|
190
|
+
result = await self._execute_single_hook(hook)
|
|
191
|
+
results.append(result)
|
|
192
|
+
self._display_hook_result(result)
|
|
193
|
+
|
|
194
|
+
if other_hooks:
|
|
195
|
+
tasks = [self._execute_single_hook(hook) for hook in other_hooks]
|
|
196
|
+
parallel_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
197
|
+
|
|
198
|
+
for i, task_result in enumerate(parallel_results):
|
|
199
|
+
if isinstance(task_result, Exception):
|
|
200
|
+
hook = other_hooks[i]
|
|
201
|
+
error_result = HookResult(
|
|
202
|
+
id=getattr(hook, "name", f"hook_{i}"),
|
|
203
|
+
name=getattr(hook, "name", f"hook_{i}"),
|
|
204
|
+
status="error",
|
|
205
|
+
duration=0.0,
|
|
206
|
+
issues_found=[str(task_result)],
|
|
207
|
+
stage=hook.stage.value,
|
|
208
|
+
)
|
|
209
|
+
results.append(error_result)
|
|
210
|
+
self._display_hook_result(error_result)
|
|
211
|
+
else:
|
|
212
|
+
hook_result = t.cast(HookResult, task_result)
|
|
213
|
+
results.append(hook_result)
|
|
214
|
+
self._display_hook_result(hook_result)
|
|
215
|
+
|
|
216
|
+
return results
|
|
217
|
+
|
|
218
|
+
async def cleanup(self) -> None:
|
|
219
|
+
"""Clean up any remaining resources before event loop closes."""
|
|
220
|
+
await self._cleanup_running_processes()
|
|
221
|
+
self._running_processes.clear()
|
|
222
|
+
await self._cleanup_pending_tasks()
|
|
223
|
+
|
|
224
|
+
async def _cleanup_running_processes(self) -> None:
|
|
225
|
+
"""Terminate all running subprocesses."""
|
|
226
|
+
for proc in list(self._running_processes):
|
|
227
|
+
await self._terminate_single_process(proc)
|
|
228
|
+
|
|
229
|
+
async def _terminate_single_process(self, proc: asyncio.subprocess.Process) -> None:
|
|
230
|
+
"""Terminate a single subprocess safely."""
|
|
231
|
+
try:
|
|
232
|
+
if proc.returncode is None:
|
|
233
|
+
proc.kill()
|
|
234
|
+
await self._wait_for_process_termination(proc)
|
|
235
|
+
except ProcessLookupError:
|
|
236
|
+
pass
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
async def _wait_for_process_termination(
|
|
241
|
+
self, proc: asyncio.subprocess.Process
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Wait briefly for process to terminate."""
|
|
244
|
+
with suppress(TimeoutError, RuntimeError):
|
|
245
|
+
await asyncio.wait_for(proc.wait(), timeout=0.1)
|
|
246
|
+
|
|
247
|
+
async def _cleanup_pending_tasks(self) -> None:
|
|
248
|
+
"""Cancel any pending hook-related tasks."""
|
|
249
|
+
with suppress(RuntimeError):
|
|
250
|
+
loop = asyncio.get_running_loop()
|
|
251
|
+
pending_tasks = self._get_pending_hook_tasks(loop)
|
|
252
|
+
await self._cancel_tasks(pending_tasks)
|
|
253
|
+
|
|
254
|
+
def _get_pending_hook_tasks(self, loop: asyncio.AbstractEventLoop) -> list:
|
|
255
|
+
"""Get list of pending hook-related tasks."""
|
|
256
|
+
return [
|
|
257
|
+
task
|
|
258
|
+
for task in asyncio.all_tasks(loop)
|
|
259
|
+
if not task.done() and "hook" in str(task).lower()
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
async def _cancel_tasks(self, tasks: list) -> None:
|
|
263
|
+
"""Cancel a list of tasks safely."""
|
|
264
|
+
for task in tasks:
|
|
265
|
+
if not task.done():
|
|
266
|
+
await self._cancel_single_task(task)
|
|
267
|
+
|
|
268
|
+
async def _cancel_single_task(self, task: asyncio.Task) -> None:
|
|
269
|
+
"""Cancel a single task safely."""
|
|
270
|
+
try:
|
|
271
|
+
task.cancel()
|
|
272
|
+
await asyncio.wait_for(task, timeout=0.1)
|
|
273
|
+
except (TimeoutError, asyncio.CancelledError):
|
|
274
|
+
pass
|
|
275
|
+
except RuntimeError as e:
|
|
276
|
+
if "Event loop is closed" in str(e):
|
|
277
|
+
return
|
|
278
|
+
else:
|
|
279
|
+
raise
|
|
280
|
+
|
|
281
|
+
async def _execute_single_hook(self, hook: HookDefinition) -> HookResult:
|
|
282
|
+
async with self._semaphore:
|
|
283
|
+
if self.hook_lock_manager.requires_lock(hook.name):
|
|
284
|
+
self.logger.debug(
|
|
285
|
+
f"Hook {hook.name} requires sequential execution lock"
|
|
286
|
+
)
|
|
287
|
+
if not self.quiet:
|
|
288
|
+
self.console.print(
|
|
289
|
+
f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if self.hook_lock_manager.requires_lock(hook.name):
|
|
293
|
+
self.logger.debug(
|
|
294
|
+
f"Hook {hook.name} requires sequential execution lock"
|
|
295
|
+
)
|
|
296
|
+
if not self.quiet:
|
|
297
|
+
self.console.print(
|
|
298
|
+
f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
async with self.hook_lock_manager.acquire_hook_lock(hook.name):
|
|
302
|
+
return await self._run_hook_subprocess(hook)
|
|
303
|
+
else:
|
|
304
|
+
return await self._run_hook_subprocess(hook)
|
|
305
|
+
|
|
306
|
+
async def _run_hook_subprocess(self, hook: HookDefinition) -> HookResult:
|
|
307
|
+
start_time = time.time()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
cmd = hook.get_command() if hasattr(hook, "get_command") else [str(hook)]
|
|
311
|
+
timeout_val = getattr(hook, "timeout", self.timeout)
|
|
312
|
+
|
|
313
|
+
self.logger.debug(
|
|
314
|
+
"Starting hook execution",
|
|
315
|
+
hook=hook.name,
|
|
316
|
+
command=" ".join(cmd),
|
|
317
|
+
timeout=timeout_val,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
repo_root = self._get_repo_root()
|
|
321
|
+
process = await asyncio.create_subprocess_exec(
|
|
322
|
+
*cmd,
|
|
323
|
+
cwd=repo_root,
|
|
324
|
+
stdout=asyncio.subprocess.PIPE,
|
|
325
|
+
stderr=asyncio.subprocess.PIPE,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Track this process for cleanup
|
|
329
|
+
self._running_processes.add(process)
|
|
330
|
+
|
|
331
|
+
result = await self._execute_process_with_timeout(
|
|
332
|
+
process, hook, timeout_val, start_time
|
|
333
|
+
)
|
|
334
|
+
if result is not None:
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
# Process completed successfully
|
|
338
|
+
duration = time.time() - start_time
|
|
339
|
+
return await self._build_success_result(process, hook, duration)
|
|
340
|
+
|
|
341
|
+
except RuntimeError as e:
|
|
342
|
+
return self._handle_runtime_error(e, hook, start_time)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
return self._handle_general_error(e, hook, start_time)
|
|
345
|
+
|
|
346
|
+
def _get_repo_root(self) -> Path:
|
|
347
|
+
"""Determine the repository root directory.
|
|
348
|
+
|
|
349
|
+
Returns pkg_path directly to ensure hooks run in the correct project directory
|
|
350
|
+
regardless of the project name.
|
|
351
|
+
"""
|
|
352
|
+
return self.pkg_path
|
|
353
|
+
|
|
354
|
+
async def _execute_process_with_timeout(
|
|
355
|
+
self,
|
|
356
|
+
process: asyncio.subprocess.Process,
|
|
357
|
+
hook: HookDefinition,
|
|
358
|
+
timeout_val: int,
|
|
359
|
+
start_time: float,
|
|
360
|
+
) -> HookResult | None:
|
|
361
|
+
"""Execute process with timeout handling. Returns HookResult on timeout, None on success."""
|
|
362
|
+
try:
|
|
363
|
+
stdout, stderr = await asyncio.wait_for(
|
|
364
|
+
process.communicate(),
|
|
365
|
+
timeout=timeout_val,
|
|
366
|
+
)
|
|
367
|
+
# Process completed normally - remove from tracking
|
|
368
|
+
self._running_processes.discard(process)
|
|
369
|
+
# Store output for later use
|
|
370
|
+
self._last_stdout = stdout
|
|
371
|
+
self._last_stderr = stderr
|
|
372
|
+
return None
|
|
373
|
+
except TimeoutError:
|
|
374
|
+
return await self._handle_process_timeout(
|
|
375
|
+
process, hook, timeout_val, start_time
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
async def _handle_process_timeout(
|
|
379
|
+
self,
|
|
380
|
+
process: asyncio.subprocess.Process,
|
|
381
|
+
hook: HookDefinition,
|
|
382
|
+
timeout_val: int,
|
|
383
|
+
start_time: float,
|
|
384
|
+
) -> HookResult:
|
|
385
|
+
"""Handle process timeout by killing process and returning timeout result."""
|
|
386
|
+
await self._terminate_process_safely(process, hook)
|
|
387
|
+
duration = time.time() - start_time
|
|
388
|
+
|
|
389
|
+
self.logger.warning(
|
|
390
|
+
"Hook execution timed out",
|
|
391
|
+
hook=hook.name,
|
|
392
|
+
timeout=timeout_val,
|
|
393
|
+
duration_seconds=round(duration, 2),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return HookResult(
|
|
397
|
+
id=hook.name,
|
|
398
|
+
name=hook.name,
|
|
399
|
+
status="timeout",
|
|
400
|
+
duration=duration,
|
|
401
|
+
issues_found=[f"Hook timed out after {duration: .1f}s"],
|
|
402
|
+
issues_count=1, # Timeout counts as 1 issue
|
|
403
|
+
stage=hook.stage.value,
|
|
404
|
+
exit_code=124, # Standard timeout exit code
|
|
405
|
+
error_message=f"Hook execution exceeded timeout of {timeout_val}s",
|
|
406
|
+
is_timeout=True,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
async def _terminate_process_safely(
|
|
410
|
+
self,
|
|
411
|
+
process: asyncio.subprocess.Process,
|
|
412
|
+
hook: HookDefinition,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Safely terminate a process and handle termination errors."""
|
|
415
|
+
try:
|
|
416
|
+
process.kill()
|
|
417
|
+
await asyncio.wait_for(process.wait(), timeout=0.1)
|
|
418
|
+
self._running_processes.discard(process)
|
|
419
|
+
except (TimeoutError, RuntimeError) as e_wait:
|
|
420
|
+
self._log_termination_error(e_wait, hook)
|
|
421
|
+
self._running_processes.discard(process)
|
|
422
|
+
|
|
423
|
+
def _log_termination_error(
|
|
424
|
+
self,
|
|
425
|
+
error: Exception,
|
|
426
|
+
hook: HookDefinition,
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Log process termination errors appropriately."""
|
|
429
|
+
error_str = str(error)
|
|
430
|
+
if "Event loop is closed" in error_str:
|
|
431
|
+
self.logger.debug(
|
|
432
|
+
"Event loop closed while waiting for process termination",
|
|
433
|
+
hook=hook.name,
|
|
434
|
+
)
|
|
435
|
+
elif "handle" in error_str.lower() or "pid" in error_str.lower():
|
|
436
|
+
self.logger.debug(
|
|
437
|
+
"Subprocess handle issue during termination",
|
|
438
|
+
hook=hook.name,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
async def _build_success_result(
|
|
442
|
+
self,
|
|
443
|
+
process: asyncio.subprocess.Process,
|
|
444
|
+
hook: HookDefinition,
|
|
445
|
+
duration: float,
|
|
446
|
+
) -> HookResult:
|
|
447
|
+
"""Build HookResult from successful process execution."""
|
|
448
|
+
output_text = self._decode_process_output(self._last_stdout, self._last_stderr)
|
|
449
|
+
return_code = process.returncode if process.returncode is not None else -1
|
|
450
|
+
parsed_output = self._parse_hook_output(return_code, output_text, hook.name)
|
|
451
|
+
|
|
452
|
+
status = "passed" if return_code == 0 else "failed"
|
|
453
|
+
|
|
454
|
+
self.logger.info(
|
|
455
|
+
"Hook execution completed",
|
|
456
|
+
hook=hook.name,
|
|
457
|
+
status=status,
|
|
458
|
+
duration_seconds=round(duration, 2),
|
|
459
|
+
return_code=process.returncode,
|
|
460
|
+
files_processed=parsed_output.get("files_processed", 0),
|
|
461
|
+
issues_count=len(parsed_output.get("issues", [])),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
issues = parsed_output.get("issues", [])
|
|
465
|
+
# If hook failed but has no parsed issues, use raw output as error details
|
|
466
|
+
if status == "failed" and not issues and output_text:
|
|
467
|
+
# Split output into lines and take first 10 non-empty lines as issues
|
|
468
|
+
error_lines = [
|
|
469
|
+
line.strip() for line in output_text.split("\n") if line.strip()
|
|
470
|
+
][:10]
|
|
471
|
+
issues = error_lines or ["Hook failed with non-zero exit code"]
|
|
472
|
+
|
|
473
|
+
# Ensure failed hooks always have at least 1 issue count
|
|
474
|
+
issues_count = max(len(issues), 1 if status == "failed" else 0)
|
|
475
|
+
|
|
476
|
+
return HookResult(
|
|
477
|
+
id=parsed_output.get("hook_id", hook.name),
|
|
478
|
+
name=hook.name,
|
|
479
|
+
status=status,
|
|
480
|
+
duration=duration,
|
|
481
|
+
files_processed=parsed_output.get("files_processed", 0),
|
|
482
|
+
issues_found=issues,
|
|
483
|
+
issues_count=issues_count,
|
|
484
|
+
stage=hook.stage.value,
|
|
485
|
+
exit_code=return_code, # Include exit code for debugging
|
|
486
|
+
error_message=output_text[:500]
|
|
487
|
+
if status == "failed" and output_text
|
|
488
|
+
else None, # First 500 chars of error
|
|
489
|
+
is_timeout=False,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _decode_process_output(self, stdout: bytes | None, stderr: bytes | None) -> str:
|
|
493
|
+
"""Decode process stdout and stderr into a single string."""
|
|
494
|
+
stdout_text = stdout.decode() if stdout else ""
|
|
495
|
+
stderr_text = stderr.decode() if stderr else ""
|
|
496
|
+
return stdout_text + stderr_text
|
|
497
|
+
|
|
498
|
+
def _handle_runtime_error(
|
|
499
|
+
self,
|
|
500
|
+
error: RuntimeError,
|
|
501
|
+
hook: HookDefinition,
|
|
502
|
+
start_time: float,
|
|
503
|
+
) -> HookResult:
|
|
504
|
+
"""Handle RuntimeError during hook execution."""
|
|
505
|
+
if "Event loop is closed" in str(error):
|
|
506
|
+
duration = time.time() - start_time
|
|
507
|
+
self.logger.warning(
|
|
508
|
+
"Event loop closed during hook execution, returning error",
|
|
509
|
+
hook=hook.name,
|
|
510
|
+
duration_seconds=round(duration, 2),
|
|
511
|
+
)
|
|
512
|
+
return HookResult(
|
|
513
|
+
id=hook.name,
|
|
514
|
+
name=hook.name,
|
|
515
|
+
status="error",
|
|
516
|
+
duration=duration,
|
|
517
|
+
issues_found=["Event loop closed during execution"],
|
|
518
|
+
issues_count=1, # Error counts as 1 issue
|
|
519
|
+
stage=hook.stage.value,
|
|
520
|
+
exit_code=1,
|
|
521
|
+
error_message="Event loop closed during hook execution",
|
|
522
|
+
is_timeout=False,
|
|
523
|
+
)
|
|
524
|
+
else:
|
|
525
|
+
raise
|
|
526
|
+
|
|
527
|
+
def _handle_general_error(
|
|
528
|
+
self,
|
|
529
|
+
error: Exception,
|
|
530
|
+
hook: HookDefinition,
|
|
531
|
+
start_time: float,
|
|
532
|
+
) -> HookResult:
|
|
533
|
+
"""Handle general exceptions during hook execution."""
|
|
534
|
+
duration = time.time() - start_time
|
|
535
|
+
self.logger.exception(
|
|
536
|
+
"Hook execution failed with exception",
|
|
537
|
+
hook=hook.name,
|
|
538
|
+
error=str(error),
|
|
539
|
+
error_type=type(error).__name__,
|
|
540
|
+
duration_seconds=round(duration, 2),
|
|
541
|
+
)
|
|
542
|
+
return HookResult(
|
|
543
|
+
id=hook.name,
|
|
544
|
+
name=hook.name,
|
|
545
|
+
status="error",
|
|
546
|
+
duration=duration,
|
|
547
|
+
issues_found=[str(error)],
|
|
548
|
+
issues_count=1, # Error counts as 1 issue
|
|
549
|
+
stage=hook.stage.value,
|
|
550
|
+
exit_code=1,
|
|
551
|
+
error_message=f"{type(error).__name__}: {error}",
|
|
552
|
+
is_timeout=False,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
def _parse_semgrep_output_async(self, output: str) -> int:
|
|
556
|
+
"""Parse Semgrep output to count files with issues, not total files scanned."""
|
|
557
|
+
|
|
558
|
+
# Try JSON parsing first
|
|
559
|
+
json_result = self._try_parse_semgrep_json(output)
|
|
560
|
+
if json_result is not None:
|
|
561
|
+
return json_result
|
|
562
|
+
|
|
563
|
+
# Fall back to text pattern matching
|
|
564
|
+
return self._parse_semgrep_text_patterns(output)
|
|
565
|
+
|
|
566
|
+
def _try_parse_semgrep_json(self, output: str) -> int | None:
|
|
567
|
+
"""Try to parse Semgrep JSON output."""
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
stripped_output = output.strip()
|
|
571
|
+
|
|
572
|
+
# Try parsing entire output as JSON
|
|
573
|
+
if stripped_output.startswith("{"):
|
|
574
|
+
count = self._extract_file_count_from_json(stripped_output)
|
|
575
|
+
if count is not None:
|
|
576
|
+
return count
|
|
577
|
+
|
|
578
|
+
# Try line-by-line JSON parsing
|
|
579
|
+
return self._parse_semgrep_json_lines(output)
|
|
580
|
+
except Exception:
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
def _extract_file_count_from_json(self, json_str: str) -> int | None:
|
|
584
|
+
"""Extract file count from JSON string."""
|
|
585
|
+
import json
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
json_data = json.loads(json_str)
|
|
589
|
+
if "results" in json_data:
|
|
590
|
+
file_paths = {
|
|
591
|
+
result.get("path") for result in json_data.get("results", [])
|
|
592
|
+
}
|
|
593
|
+
return len([p for p in file_paths if p])
|
|
594
|
+
except json.JSONDecodeError:
|
|
595
|
+
pass
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
def _parse_semgrep_json_lines(self, output: str) -> int | None:
|
|
599
|
+
"""Parse JSON from individual lines in output."""
|
|
600
|
+
|
|
601
|
+
lines = output.splitlines()
|
|
602
|
+
for line in lines:
|
|
603
|
+
line = line.strip()
|
|
604
|
+
if line.startswith("{") and line.endswith("}"):
|
|
605
|
+
count = self._extract_file_count_from_json(line)
|
|
606
|
+
if count is not None:
|
|
607
|
+
return count
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
def _parse_semgrep_text_patterns(self, output: str) -> int:
|
|
611
|
+
"""Parse Semgrep text output using regex patterns."""
|
|
612
|
+
import re
|
|
613
|
+
|
|
614
|
+
semgrep_patterns = [
|
|
615
|
+
r"found\s+(\d+)\s+issues?\s+in\s+(\d+)\s+files?",
|
|
616
|
+
r"found\s+no\s+issues",
|
|
617
|
+
r"scanning\s+(\d+)\s+files?",
|
|
618
|
+
]
|
|
619
|
+
|
|
620
|
+
for pattern in semgrep_patterns:
|
|
621
|
+
matches = re.findall(pattern, output, re.IGNORECASE)
|
|
622
|
+
if matches:
|
|
623
|
+
result = self._process_semgrep_matches(matches, output)
|
|
624
|
+
if result is not None:
|
|
625
|
+
return result
|
|
626
|
+
|
|
627
|
+
return 0
|
|
628
|
+
|
|
629
|
+
def _process_semgrep_matches(self, matches: list, output: str) -> int | None:
|
|
630
|
+
"""Process regex matches from Semgrep output."""
|
|
631
|
+
for match in matches:
|
|
632
|
+
if isinstance(match, tuple):
|
|
633
|
+
if len(match) == 2:
|
|
634
|
+
issue_count, file_count = int(match[0]), int(match[1])
|
|
635
|
+
return file_count if issue_count > 0 else 0
|
|
636
|
+
elif len(match) == 1 and "no issues" not in output.lower():
|
|
637
|
+
continue
|
|
638
|
+
elif "no issues" in output.lower():
|
|
639
|
+
return 0
|
|
640
|
+
return None
|
|
641
|
+
|
|
642
|
+
def _parse_semgrep_issues_async(self, output: str) -> list[str]:
|
|
643
|
+
"""Parse semgrep JSON output to extract both findings and errors.
|
|
644
|
+
|
|
645
|
+
Semgrep returns JSON with two arrays:
|
|
646
|
+
- "results": Security/code quality findings
|
|
647
|
+
- "errors": Configuration, download, or execution errors
|
|
648
|
+
|
|
649
|
+
This method extracts issues from both arrays to provide comprehensive error reporting.
|
|
650
|
+
"""
|
|
651
|
+
import json
|
|
652
|
+
|
|
653
|
+
issues = []
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
# Try to parse as JSON
|
|
657
|
+
json_data = json.loads(output.strip())
|
|
658
|
+
|
|
659
|
+
# Extract findings from results array
|
|
660
|
+
if "results" in json_data:
|
|
661
|
+
for result in json_data.get("results", []):
|
|
662
|
+
# Format: "file.py:line - rule_id: message"
|
|
663
|
+
path = result.get("path", "unknown")
|
|
664
|
+
line_num = result.get("start", {}).get("line", "?")
|
|
665
|
+
rule_id = result.get("check_id", "unknown-rule")
|
|
666
|
+
message = result.get("extra", {}).get(
|
|
667
|
+
"message", "Security issue detected"
|
|
668
|
+
)
|
|
669
|
+
issues.append(f"{path}:{line_num} - {rule_id}: {message}")
|
|
670
|
+
|
|
671
|
+
# Extract errors from errors array (config errors, download failures, etc.)
|
|
672
|
+
if "errors" in json_data:
|
|
673
|
+
for error in json_data.get("errors", []):
|
|
674
|
+
error_type = error.get("type", "SemgrepError")
|
|
675
|
+
error_msg = error.get("message", str(error))
|
|
676
|
+
issues.append(f"{error_type}: {error_msg}")
|
|
677
|
+
|
|
678
|
+
except json.JSONDecodeError:
|
|
679
|
+
# If JSON parsing fails, return raw output (shouldn't happen with --json flag)
|
|
680
|
+
if output.strip():
|
|
681
|
+
issues = [line.strip() for line in output.split("\n") if line.strip()][
|
|
682
|
+
:10
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
return issues
|
|
686
|
+
|
|
687
|
+
def _parse_hook_output(
|
|
688
|
+
self, returncode: int, output: str, hook_name: str = ""
|
|
689
|
+
) -> dict[str, t.Any]:
|
|
690
|
+
"""Parse hook output to extract file counts and other metrics.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
returncode: Exit code from the subprocess
|
|
694
|
+
output: Raw output from the hook execution
|
|
695
|
+
hook_name: Name of the hook being executed to allow special handling
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
Dictionary with parsed results including files_processed
|
|
699
|
+
"""
|
|
700
|
+
result = self._initialize_parse_result(returncode, output)
|
|
701
|
+
|
|
702
|
+
# Special handling for semgrep
|
|
703
|
+
if hook_name == "semgrep":
|
|
704
|
+
result["files_processed"] = self._parse_semgrep_output_async(output)
|
|
705
|
+
result["issues"] = self._parse_semgrep_issues_async(output)
|
|
706
|
+
return result
|
|
707
|
+
|
|
708
|
+
# Special handling for check-added-large-files
|
|
709
|
+
if hook_name == "check-added-large-files":
|
|
710
|
+
result["files_processed"] = self._parse_large_files_output(
|
|
711
|
+
output, returncode
|
|
712
|
+
)
|
|
713
|
+
return result
|
|
714
|
+
|
|
715
|
+
# General hook parsing
|
|
716
|
+
result["files_processed"] = self._extract_file_count_from_output(output)
|
|
717
|
+
return result
|
|
718
|
+
|
|
719
|
+
def _initialize_parse_result(
|
|
720
|
+
self, returncode: int, output: str
|
|
721
|
+
) -> dict[str, t.Any]:
|
|
722
|
+
"""Initialize result dictionary with default values."""
|
|
723
|
+
return {
|
|
724
|
+
"hook_id": None,
|
|
725
|
+
"exit_code": returncode,
|
|
726
|
+
"files_processed": 0,
|
|
727
|
+
"issues": [],
|
|
728
|
+
"raw_output": output,
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
def _parse_large_files_output(self, output: str, returncode: int) -> int:
|
|
732
|
+
"""Parse check-added-large-files output to count files exceeding size limit."""
|
|
733
|
+
|
|
734
|
+
clean_output = output.replace("\\n", "\n").replace("\\t", "\t")
|
|
735
|
+
|
|
736
|
+
# Try to find explicit failure patterns
|
|
737
|
+
failure_count = self._find_large_file_failures(clean_output)
|
|
738
|
+
if failure_count is not None:
|
|
739
|
+
return failure_count
|
|
740
|
+
|
|
741
|
+
# Check for "all files under limit" success case
|
|
742
|
+
if self._is_all_files_under_limit(clean_output, returncode):
|
|
743
|
+
return 0
|
|
744
|
+
|
|
745
|
+
# If hook failed but no pattern matched, assume at least 1 file failed
|
|
746
|
+
if returncode != 0:
|
|
747
|
+
return 1
|
|
748
|
+
|
|
749
|
+
# Default: no large files found
|
|
750
|
+
return 0
|
|
751
|
+
|
|
752
|
+
def _find_large_file_failures(self, clean_output: str) -> int | None:
|
|
753
|
+
"""Find count of files that exceeded size limit."""
|
|
754
|
+
import re
|
|
755
|
+
|
|
756
|
+
failure_patterns = [
|
|
757
|
+
r"large file(?:s)? found:?\s*(\d+)",
|
|
758
|
+
r"found\s+(\d+)\s+large file",
|
|
759
|
+
r"(\d+)\s+file(?:s)?\s+exceed(?:ed)?\s+size\s+limit",
|
|
760
|
+
r"(\d+)\s+large file(?:s)?\s+found",
|
|
761
|
+
r"(\d+)\s+file(?:s)?\s+(?:failed|violated|exceeded)",
|
|
762
|
+
]
|
|
763
|
+
|
|
764
|
+
for pattern in failure_patterns:
|
|
765
|
+
matches = re.findall(pattern, clean_output, re.IGNORECASE)
|
|
766
|
+
if matches:
|
|
767
|
+
return int(max([int(m) for m in matches if m.isdigit()]))
|
|
768
|
+
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
def _is_all_files_under_limit(self, clean_output: str, returncode: int) -> bool:
|
|
772
|
+
"""Check if output indicates all files are under size limit."""
|
|
773
|
+
import re
|
|
774
|
+
|
|
775
|
+
pattern = r"All files are under size limit"
|
|
776
|
+
return bool(re.search(pattern, clean_output, re.IGNORECASE) and returncode == 0)
|
|
777
|
+
|
|
778
|
+
def _extract_file_count_from_output(self, output: str) -> int:
|
|
779
|
+
"""Extract file count from general hook output."""
|
|
780
|
+
import re
|
|
781
|
+
|
|
782
|
+
clean_output = output.replace("\\n", "\n").replace("\\t", "\t")
|
|
783
|
+
patterns = self._get_file_count_patterns()
|
|
784
|
+
|
|
785
|
+
all_matches = []
|
|
786
|
+
for pattern in patterns:
|
|
787
|
+
matches = re.findall(pattern, clean_output, re.IGNORECASE)
|
|
788
|
+
if matches:
|
|
789
|
+
all_matches.extend([int(m) for m in matches if m.isdigit()])
|
|
790
|
+
|
|
791
|
+
return max(all_matches) if all_matches else 0
|
|
792
|
+
|
|
793
|
+
def _get_file_count_patterns(self) -> list[str]:
|
|
794
|
+
"""Get regex patterns for extracting file counts from hook output."""
|
|
795
|
+
return [
|
|
796
|
+
r"(\d+)\s+files?\s+(?:processed|checked|examined|scanned|formatted|found|affected)",
|
|
797
|
+
r"found\s+(\d+)\s+files?",
|
|
798
|
+
r"(\d+)\s+files?\s+with\s+issues?",
|
|
799
|
+
r"(\d+)\s+files?\s+(?:would\s+be|were)\s+(?:formatted|modified|fixed)",
|
|
800
|
+
r"(\d+)\s+files?\s+would\s+be\s+?(?:formatted|fixed|updated)",
|
|
801
|
+
r"(\d+)\s+files?\s+?(?:formatted|fixed|updated)",
|
|
802
|
+
r"(\d+)\s+files?\s+formatted",
|
|
803
|
+
r"analyzed\s+(\d+)\s+deps",
|
|
804
|
+
r"(\d+)\s+findings?",
|
|
805
|
+
r"(\d+)\s+issues?\s+found",
|
|
806
|
+
r"(\d+)\s+tests ran",
|
|
807
|
+
r"(\d+)\s+files\s+scanned",
|
|
808
|
+
r"Checked\s+(\d+)\s+files?",
|
|
809
|
+
r"for\s+(\d+)\s+files?",
|
|
810
|
+
r"(\d+)\s+files?",
|
|
811
|
+
]
|
|
812
|
+
|
|
813
|
+
def _display_hook_result(self, result: HookResult) -> None:
|
|
814
|
+
if self.quiet:
|
|
815
|
+
return
|
|
816
|
+
width = get_console_width()
|
|
817
|
+
dots = "." * max(0, (width - len(result.name)))
|
|
818
|
+
status_text = "Passed" if result.status == "passed" else "Failed"
|
|
819
|
+
status_color = "green" if result.status == "passed" else "red"
|
|
820
|
+
|
|
821
|
+
self.console.print(
|
|
822
|
+
f"{result.name}{dots}[{status_color}]{status_text}[/{status_color}]"
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
if result.status != "passed" and result.issues_found:
|
|
826
|
+
for issue in result.issues_found:
|
|
827
|
+
if issue and "raw_output" not in issue:
|
|
828
|
+
self.console.print(issue)
|
|
829
|
+
|
|
830
|
+
async def _handle_retries(
|
|
831
|
+
self,
|
|
832
|
+
strategy: HookStrategy,
|
|
833
|
+
results: list[HookResult],
|
|
834
|
+
) -> list[HookResult]:
|
|
835
|
+
if strategy.retry_policy == RetryPolicy.FORMATTING_ONLY:
|
|
836
|
+
return await self._retry_formatting_hooks(strategy, results)
|
|
837
|
+
if strategy.retry_policy == RetryPolicy.ALL_HOOKS:
|
|
838
|
+
return await self._retry_all_hooks(strategy, results)
|
|
839
|
+
return results
|
|
840
|
+
|
|
841
|
+
async def _retry_formatting_hooks(
|
|
842
|
+
self,
|
|
843
|
+
strategy: HookStrategy,
|
|
844
|
+
results: list[HookResult],
|
|
845
|
+
) -> list[HookResult]:
|
|
846
|
+
formatting_hooks_failed: set[str] = set()
|
|
847
|
+
|
|
848
|
+
for i, result in enumerate(results):
|
|
849
|
+
hook = strategy.hooks[i]
|
|
850
|
+
if getattr(hook, "is_formatting", False) and result.status == "failed":
|
|
851
|
+
formatting_hooks_failed.add(hook.name)
|
|
852
|
+
|
|
853
|
+
if not formatting_hooks_failed:
|
|
854
|
+
return results
|
|
855
|
+
|
|
856
|
+
retry_tasks = [self._execute_single_hook(hook) for hook in strategy.hooks]
|
|
857
|
+
retry_results = await asyncio.gather(*retry_tasks, return_exceptions=True)
|
|
858
|
+
|
|
859
|
+
updated_results: list[HookResult] = []
|
|
860
|
+
for i, (prev_result, new_result) in enumerate(
|
|
861
|
+
zip(results, retry_results, strict=False)
|
|
862
|
+
):
|
|
863
|
+
if isinstance(new_result, Exception):
|
|
864
|
+
hook = strategy.hooks[i]
|
|
865
|
+
error_result = HookResult(
|
|
866
|
+
id=hook.name,
|
|
867
|
+
name=hook.name,
|
|
868
|
+
status="error",
|
|
869
|
+
duration=prev_result.duration,
|
|
870
|
+
issues_found=[str(new_result)],
|
|
871
|
+
stage=hook.stage.value,
|
|
872
|
+
)
|
|
873
|
+
updated_results.append(error_result)
|
|
874
|
+
else:
|
|
875
|
+
hook_result = t.cast("HookResult", new_result)
|
|
876
|
+
hook_result.duration += prev_result.duration
|
|
877
|
+
updated_results.append(hook_result)
|
|
878
|
+
|
|
879
|
+
self._display_hook_result(updated_results[-1])
|
|
880
|
+
|
|
881
|
+
return updated_results
|
|
882
|
+
|
|
883
|
+
async def _retry_all_hooks(
|
|
884
|
+
self,
|
|
885
|
+
strategy: HookStrategy,
|
|
886
|
+
results: list[HookResult],
|
|
887
|
+
) -> list[HookResult]:
|
|
888
|
+
failed_indices = [i for i, r in enumerate(results) if r.status == "failed"]
|
|
889
|
+
|
|
890
|
+
if not failed_indices:
|
|
891
|
+
return results
|
|
892
|
+
|
|
893
|
+
updated_results = results.copy()
|
|
894
|
+
retry_tasks: list[t.Awaitable[HookResult]] = []
|
|
895
|
+
retry_indices: list[int] = []
|
|
896
|
+
|
|
897
|
+
for i in failed_indices:
|
|
898
|
+
hook = strategy.hooks[i]
|
|
899
|
+
retry_tasks.append(self._execute_single_hook(hook))
|
|
900
|
+
retry_indices.append(i)
|
|
901
|
+
|
|
902
|
+
retry_results = await asyncio.gather(*retry_tasks, return_exceptions=True)
|
|
903
|
+
|
|
904
|
+
for result_idx, new_result in zip(retry_indices, retry_results, strict=False):
|
|
905
|
+
prev_result = results[result_idx]
|
|
906
|
+
|
|
907
|
+
if isinstance(new_result, Exception):
|
|
908
|
+
hook = strategy.hooks[result_idx]
|
|
909
|
+
error_result = HookResult(
|
|
910
|
+
id=hook.name,
|
|
911
|
+
name=hook.name,
|
|
912
|
+
status="error",
|
|
913
|
+
duration=prev_result.duration,
|
|
914
|
+
issues_found=[str(new_result)],
|
|
915
|
+
stage=hook.stage.value,
|
|
916
|
+
)
|
|
917
|
+
updated_results[result_idx] = error_result
|
|
918
|
+
else:
|
|
919
|
+
hook_result = t.cast("HookResult", new_result)
|
|
920
|
+
hook_result.duration += prev_result.duration
|
|
921
|
+
updated_results[result_idx] = hook_result
|
|
922
|
+
|
|
923
|
+
self._display_hook_result(updated_results[result_idx])
|
|
924
|
+
|
|
925
|
+
return updated_results
|
|
926
|
+
|
|
927
|
+
def _print_summary(
|
|
928
|
+
self,
|
|
929
|
+
strategy: HookStrategy,
|
|
930
|
+
results: list[HookResult],
|
|
931
|
+
success: bool,
|
|
932
|
+
performance_gain: float,
|
|
933
|
+
) -> None:
|
|
934
|
+
if success:
|
|
935
|
+
self.console.print(
|
|
936
|
+
f"[green]✅[/ green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)} "
|
|
937
|
+
f"(async, {performance_gain: .1f} % faster)",
|
|
938
|
+
)
|