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,1227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import typing as t
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from acb.console import Console
|
|
8
|
+
from acb.depends import Inject, depends
|
|
9
|
+
from acb.logger import Logger
|
|
10
|
+
from rich import box
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.progress import (
|
|
13
|
+
BarColumn,
|
|
14
|
+
MofNCompleteColumn,
|
|
15
|
+
Progress,
|
|
16
|
+
SpinnerColumn,
|
|
17
|
+
TextColumn,
|
|
18
|
+
TimeElapsedColumn,
|
|
19
|
+
)
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
|
|
22
|
+
from crackerjack.cli.formatting import separator as make_separator
|
|
23
|
+
from crackerjack.code_cleaner import CodeCleaner
|
|
24
|
+
from crackerjack.config import get_console_width
|
|
25
|
+
from crackerjack.core.autofix_coordinator import AutofixCoordinator
|
|
26
|
+
from crackerjack.core.session_coordinator import SessionCoordinator
|
|
27
|
+
from crackerjack.decorators import handle_errors
|
|
28
|
+
from crackerjack.models.protocols import (
|
|
29
|
+
ConfigMergeServiceProtocol,
|
|
30
|
+
FileSystemInterface,
|
|
31
|
+
GitInterface,
|
|
32
|
+
HookManager,
|
|
33
|
+
MemoryOptimizerProtocol,
|
|
34
|
+
OptionsProtocol,
|
|
35
|
+
PublishManager,
|
|
36
|
+
TestManagerProtocol,
|
|
37
|
+
)
|
|
38
|
+
from crackerjack.models.task import HookResult
|
|
39
|
+
from crackerjack.services.memory_optimizer import create_lazy_service
|
|
40
|
+
from crackerjack.services.monitoring.performance_cache import (
|
|
41
|
+
FileSystemCache,
|
|
42
|
+
GitOperationCache,
|
|
43
|
+
)
|
|
44
|
+
from crackerjack.services.parallel_executor import (
|
|
45
|
+
AsyncCommandExecutor,
|
|
46
|
+
ParallelHookExecutor,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if t.TYPE_CHECKING:
|
|
50
|
+
pass # All imports moved to top-level for runtime availability
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PhaseCoordinator:
|
|
54
|
+
@depends.inject
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
console: Inject[Console],
|
|
58
|
+
logger: Inject[Logger],
|
|
59
|
+
memory_optimizer: Inject[MemoryOptimizerProtocol],
|
|
60
|
+
parallel_executor: Inject[ParallelHookExecutor],
|
|
61
|
+
async_executor: Inject[AsyncCommandExecutor],
|
|
62
|
+
git_cache: Inject[GitOperationCache],
|
|
63
|
+
filesystem_cache: Inject[FileSystemCache],
|
|
64
|
+
pkg_path: Inject[Path],
|
|
65
|
+
session: Inject[SessionCoordinator],
|
|
66
|
+
filesystem: Inject[FileSystemInterface],
|
|
67
|
+
git_service: Inject[GitInterface],
|
|
68
|
+
hook_manager: Inject[HookManager],
|
|
69
|
+
test_manager: Inject[TestManagerProtocol],
|
|
70
|
+
publish_manager: Inject[PublishManager],
|
|
71
|
+
config_merge_service: Inject[ConfigMergeServiceProtocol],
|
|
72
|
+
) -> None:
|
|
73
|
+
self.console = console
|
|
74
|
+
self.pkg_path = pkg_path
|
|
75
|
+
self.session = session
|
|
76
|
+
|
|
77
|
+
# Dependencies injected via ACB's depends.get() from WorkflowOrchestrator
|
|
78
|
+
self.filesystem = filesystem
|
|
79
|
+
self.git_service = git_service
|
|
80
|
+
self.hook_manager = hook_manager
|
|
81
|
+
self.test_manager = test_manager
|
|
82
|
+
self.publish_manager = publish_manager
|
|
83
|
+
self.config_merge_service = config_merge_service
|
|
84
|
+
|
|
85
|
+
self.code_cleaner = CodeCleaner(
|
|
86
|
+
console=console,
|
|
87
|
+
base_directory=pkg_path,
|
|
88
|
+
file_processor=None,
|
|
89
|
+
error_handler=None,
|
|
90
|
+
pipeline=None,
|
|
91
|
+
logger=None,
|
|
92
|
+
security_logger=None,
|
|
93
|
+
backup_service=None,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Ensure logger is a proper instance, not an empty tuple, string, or other invalid value
|
|
97
|
+
if isinstance(logger, tuple) and len(logger) == 0:
|
|
98
|
+
# Log this issue for debugging
|
|
99
|
+
print(
|
|
100
|
+
"WARNING: PhaseCoordinator received empty tuple for logger dependency, creating fallback"
|
|
101
|
+
)
|
|
102
|
+
# Import and create a fallback logger if we got an empty tuple
|
|
103
|
+
from acb.logger import Logger as ACBLogger
|
|
104
|
+
|
|
105
|
+
self._logger = ACBLogger()
|
|
106
|
+
elif isinstance(logger, str):
|
|
107
|
+
# Log this issue for debugging
|
|
108
|
+
print(
|
|
109
|
+
f"WARNING: PhaseCoordinator received string for logger dependency: {logger!r}, creating fallback"
|
|
110
|
+
)
|
|
111
|
+
# Import and create a fallback logger if we got a string
|
|
112
|
+
from acb.logger import Logger as ACBLogger
|
|
113
|
+
|
|
114
|
+
self._logger = ACBLogger()
|
|
115
|
+
else:
|
|
116
|
+
self._logger = logger
|
|
117
|
+
|
|
118
|
+
# Services injected via ACB DI
|
|
119
|
+
self._memory_optimizer = memory_optimizer
|
|
120
|
+
self._parallel_executor = parallel_executor
|
|
121
|
+
self._async_executor = async_executor
|
|
122
|
+
self._git_cache = git_cache
|
|
123
|
+
self._filesystem_cache = filesystem_cache
|
|
124
|
+
|
|
125
|
+
self._last_hook_summary: dict[str, t.Any] | None = None
|
|
126
|
+
self._last_hook_results: list[HookResult] = []
|
|
127
|
+
|
|
128
|
+
self._lazy_autofix = create_lazy_service(
|
|
129
|
+
lambda: AutofixCoordinator(pkg_path=pkg_path),
|
|
130
|
+
"autofix_coordinator",
|
|
131
|
+
)
|
|
132
|
+
self.console.print()
|
|
133
|
+
|
|
134
|
+
# Track if fast hooks have already started in this session to prevent duplicates
|
|
135
|
+
self._fast_hooks_started: bool = False
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def logger(self) -> Logger:
|
|
139
|
+
"""Safely access the logger instance, ensuring it's not an empty tuple or string."""
|
|
140
|
+
if hasattr(self, "_logger") and (
|
|
141
|
+
(isinstance(self._logger, tuple) and len(self._logger) == 0)
|
|
142
|
+
or isinstance(self._logger, str)
|
|
143
|
+
):
|
|
144
|
+
from acb.logger import Logger as ACBLogger
|
|
145
|
+
|
|
146
|
+
print(
|
|
147
|
+
f"WARNING: PhaseCoordinator logger was invalid type ({type(self._logger).__name__}: {self._logger!r}), creating fresh logger instance"
|
|
148
|
+
)
|
|
149
|
+
self._logger = ACBLogger()
|
|
150
|
+
return self._logger
|
|
151
|
+
|
|
152
|
+
@logger.setter
|
|
153
|
+
def logger(self, value: Logger) -> None:
|
|
154
|
+
"""Set the logger instance."""
|
|
155
|
+
self._logger = value
|
|
156
|
+
|
|
157
|
+
# --- Output/formatting helpers -------------------------------------------------
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _strip_ansi(text: str) -> str:
|
|
160
|
+
"""Remove ANSI escape sequences (SGR and cursor controls).
|
|
161
|
+
|
|
162
|
+
This is more comprehensive than stripping only color codes ending with 'm'.
|
|
163
|
+
"""
|
|
164
|
+
ansi_re = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
165
|
+
return ansi_re.sub("", text)
|
|
166
|
+
|
|
167
|
+
def _is_plain_output(self) -> bool:
|
|
168
|
+
"""Detect if we should avoid rich formatting entirely.
|
|
169
|
+
|
|
170
|
+
Leverages ACB Console's plain-mode flag when available and falls back
|
|
171
|
+
to Rich Console properties when not.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
if bool(getattr(self.console, "_plain_mode", False)):
|
|
175
|
+
return True
|
|
176
|
+
# Fallback on Rich Console capabilities
|
|
177
|
+
is_tty = bool(getattr(self.console, "is_terminal", True))
|
|
178
|
+
color_system = getattr(self.console, "color_system", None)
|
|
179
|
+
return (not is_tty) or (color_system in (None, "null"))
|
|
180
|
+
except Exception:
|
|
181
|
+
# Prefer plain in ambiguous environments
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
@handle_errors
|
|
185
|
+
def run_cleaning_phase(self, options: OptionsProtocol) -> bool:
|
|
186
|
+
if not options.clean:
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
self.session.track_task("cleaning", "Code cleaning")
|
|
190
|
+
self._display_cleaning_header()
|
|
191
|
+
return self._execute_cleaning_process()
|
|
192
|
+
|
|
193
|
+
@handle_errors
|
|
194
|
+
def run_configuration_phase(self, options: OptionsProtocol) -> bool:
|
|
195
|
+
if options.no_config_updates:
|
|
196
|
+
return True
|
|
197
|
+
self.session.track_task("configuration", "Configuration updates")
|
|
198
|
+
self.console.print(
|
|
199
|
+
"[dim]⚙️ Configuration phase skipped (no automated updates defined).[/dim]"
|
|
200
|
+
)
|
|
201
|
+
self.session.complete_task(
|
|
202
|
+
"configuration", "No configuration updates were required."
|
|
203
|
+
)
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
@handle_errors
|
|
207
|
+
def run_hooks_phase(self, options: OptionsProtocol) -> bool:
|
|
208
|
+
if options.skip_hooks:
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
if not self.run_fast_hooks_only(options):
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
return self.run_comprehensive_hooks_only(options)
|
|
215
|
+
|
|
216
|
+
def run_fast_hooks_only(self, options: OptionsProtocol) -> bool:
|
|
217
|
+
if options.skip_hooks:
|
|
218
|
+
self.console.print("[yellow]⚠️[/yellow] Skipping fast hooks (--skip-hooks)")
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
# Prevent multiple fast-hook runs in a single workflow session unless
|
|
222
|
+
# explicitly reset by post-cleaning sanity check.
|
|
223
|
+
if getattr(self, "_fast_hooks_started", False):
|
|
224
|
+
self.logger.debug("Duplicate fast hooks invocation detected; skipping")
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
# Mark fast hooks as started immediately to prevent duplicate calls in case of failures
|
|
228
|
+
self._fast_hooks_started = True
|
|
229
|
+
self.session.track_task("hooks_fast", "Fast quality checks")
|
|
230
|
+
|
|
231
|
+
# Fast hooks get 2 attempts (auto-fix on failure), comprehensive hooks run once
|
|
232
|
+
max_attempts = 2
|
|
233
|
+
attempt = 0
|
|
234
|
+
|
|
235
|
+
while attempt < max_attempts:
|
|
236
|
+
attempt += 1
|
|
237
|
+
|
|
238
|
+
# Display stage header for each attempt
|
|
239
|
+
if attempt > 1:
|
|
240
|
+
self.console.print(
|
|
241
|
+
f"\n[yellow]♻️[/yellow] Verification Retry {attempt}/{max_attempts}\n"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self._display_hook_phase_header(
|
|
245
|
+
"FAST HOOKS",
|
|
246
|
+
"Formatters, import sorting, and quick static analysis",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Run hooks (now configured to run in fix mode by default)
|
|
250
|
+
success = self._execute_hooks_once(
|
|
251
|
+
"fast", self.hook_manager.run_fast_hooks, options, attempt
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if success:
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
# Fast iteration mode intentionally avoids retries
|
|
258
|
+
if getattr(options, "fast_iteration", False):
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
# If we have more attempts, continue to retry to verify fixes worked
|
|
262
|
+
if attempt < max_attempts:
|
|
263
|
+
self._display_hook_failures("fast", self._last_hook_results, options)
|
|
264
|
+
|
|
265
|
+
summary = self._last_hook_summary or {}
|
|
266
|
+
details = self._format_hook_summary(summary)
|
|
267
|
+
|
|
268
|
+
if success:
|
|
269
|
+
self.session.complete_task("hooks_fast", details=details)
|
|
270
|
+
else:
|
|
271
|
+
self.session.fail_task("hooks_fast", "Fast hook failures detected")
|
|
272
|
+
|
|
273
|
+
# Ensure fast hooks output is fully rendered before comprehensive hooks start
|
|
274
|
+
self.console.print()
|
|
275
|
+
|
|
276
|
+
return success
|
|
277
|
+
|
|
278
|
+
def run_comprehensive_hooks_only(self, options: OptionsProtocol) -> bool:
|
|
279
|
+
if options.skip_hooks:
|
|
280
|
+
self.console.print(
|
|
281
|
+
"[yellow]⚠️[/yellow] Skipping comprehensive hooks (--skip-hooks)"
|
|
282
|
+
)
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
self.session.track_task("hooks_comprehensive", "Comprehensive quality checks")
|
|
286
|
+
self._display_hook_phase_header(
|
|
287
|
+
"COMPREHENSIVE HOOKS",
|
|
288
|
+
"Type, security, and complexity checking",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Comprehensive hooks run once (no retry)
|
|
292
|
+
success = self._execute_hooks_once(
|
|
293
|
+
"comprehensive",
|
|
294
|
+
self.hook_manager.run_comprehensive_hooks,
|
|
295
|
+
options,
|
|
296
|
+
attempt=1,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if not success:
|
|
300
|
+
self._display_hook_failures(
|
|
301
|
+
"comprehensive", self._last_hook_results, options
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
summary = self._last_hook_summary or {}
|
|
305
|
+
details = self._format_hook_summary(summary)
|
|
306
|
+
|
|
307
|
+
if success:
|
|
308
|
+
self.session.complete_task("hooks_comprehensive", details=details)
|
|
309
|
+
else:
|
|
310
|
+
self.session.fail_task(
|
|
311
|
+
"hooks_comprehensive", "Comprehensive hook failures detected"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return success
|
|
315
|
+
|
|
316
|
+
@handle_errors
|
|
317
|
+
def run_testing_phase(self, options: OptionsProtocol) -> bool:
|
|
318
|
+
if not options.test:
|
|
319
|
+
return True
|
|
320
|
+
self.session.track_task("testing", "Test execution")
|
|
321
|
+
self.console.print("\n" + make_separator("-"))
|
|
322
|
+
self.console.print(
|
|
323
|
+
"[bold bright_blue]🧪 TESTS[/bold bright_blue] [bold bright_white]Running test suite[/bold bright_white]",
|
|
324
|
+
)
|
|
325
|
+
self.console.print(make_separator("-") + "\n")
|
|
326
|
+
if not self.test_manager.validate_test_environment():
|
|
327
|
+
self.session.fail_task("testing", "Test environment validation failed")
|
|
328
|
+
return False
|
|
329
|
+
test_success = self.test_manager.run_tests(options)
|
|
330
|
+
if test_success:
|
|
331
|
+
coverage_info = self.test_manager.get_coverage()
|
|
332
|
+
self.session.complete_task(
|
|
333
|
+
"testing",
|
|
334
|
+
f"Tests passed, coverage: {coverage_info.get('total_coverage', 0):.1f}%",
|
|
335
|
+
)
|
|
336
|
+
else:
|
|
337
|
+
self.session.fail_task("testing", "Tests failed")
|
|
338
|
+
|
|
339
|
+
return test_success
|
|
340
|
+
|
|
341
|
+
@handle_errors
|
|
342
|
+
def run_publishing_phase(self, options: OptionsProtocol) -> bool:
|
|
343
|
+
version_type = self._determine_version_type(options)
|
|
344
|
+
if not version_type:
|
|
345
|
+
return True
|
|
346
|
+
|
|
347
|
+
self.session.track_task("publishing", f"Publishing ({version_type})")
|
|
348
|
+
return self._execute_publishing_workflow(options, version_type)
|
|
349
|
+
|
|
350
|
+
@handle_errors
|
|
351
|
+
def run_commit_phase(self, options: OptionsProtocol) -> bool:
|
|
352
|
+
if not options.commit:
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
# Skip if publishing phase already handled commits
|
|
356
|
+
# (Publishing phase handles both pre-publish and version-bump commits when -c is used)
|
|
357
|
+
version_type = self._determine_version_type(options)
|
|
358
|
+
if version_type:
|
|
359
|
+
# Publishing workflow already committed everything
|
|
360
|
+
self.console.print(
|
|
361
|
+
"[dim]ℹ️ Commit phase skipped (handled by publish workflow)[/dim]"
|
|
362
|
+
)
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
# Display commit & push header
|
|
366
|
+
self._display_commit_push_header()
|
|
367
|
+
self.session.track_task("commit", "Git commit and push")
|
|
368
|
+
changed_files = self.git_service.get_changed_files()
|
|
369
|
+
if not changed_files:
|
|
370
|
+
return self._handle_no_changes_to_commit()
|
|
371
|
+
commit_message = self._get_commit_message(changed_files, options)
|
|
372
|
+
return self._execute_commit_and_push(changed_files, commit_message)
|
|
373
|
+
|
|
374
|
+
def _execute_hooks_once(
|
|
375
|
+
self,
|
|
376
|
+
suite_name: str,
|
|
377
|
+
hook_runner: t.Callable[[], list[HookResult]],
|
|
378
|
+
options: OptionsProtocol,
|
|
379
|
+
attempt: int,
|
|
380
|
+
) -> bool:
|
|
381
|
+
"""Execute a hook suite once with progress bar (no retry logic - retry is handled at stage level)."""
|
|
382
|
+
self._last_hook_summary = None
|
|
383
|
+
self._last_hook_results = []
|
|
384
|
+
|
|
385
|
+
hook_count = self.hook_manager.get_hook_count(suite_name)
|
|
386
|
+
progress = self._create_progress_bar()
|
|
387
|
+
|
|
388
|
+
callbacks = self._setup_progress_callbacks(progress)
|
|
389
|
+
elapsed_time = self._run_hooks_with_progress(
|
|
390
|
+
suite_name, hook_runner, progress, hook_count, attempt, callbacks
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if elapsed_time is None:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
return self._process_hook_results(suite_name, elapsed_time, attempt)
|
|
397
|
+
|
|
398
|
+
def _create_progress_bar(self) -> Progress:
|
|
399
|
+
"""Create compact progress bar for hook execution."""
|
|
400
|
+
return Progress(
|
|
401
|
+
SpinnerColumn(spinner_name="dots"),
|
|
402
|
+
TextColumn("[cyan]{task.description}[/cyan]"),
|
|
403
|
+
BarColumn(bar_width=20),
|
|
404
|
+
MofNCompleteColumn(),
|
|
405
|
+
TimeElapsedColumn(),
|
|
406
|
+
console=self.console,
|
|
407
|
+
transient=True,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def _setup_progress_callbacks(
|
|
411
|
+
self, progress: Progress
|
|
412
|
+
) -> dict[str, t.Callable[[int, int], None] | None | dict[str, t.Any]]:
|
|
413
|
+
"""Setup progress callbacks and store originals for restoration."""
|
|
414
|
+
task_id_holder = {"task_id": None}
|
|
415
|
+
|
|
416
|
+
def update_progress(completed: int, total: int) -> None:
|
|
417
|
+
if task_id_holder["task_id"] is not None:
|
|
418
|
+
progress.update(task_id_holder["task_id"], completed=completed)
|
|
419
|
+
|
|
420
|
+
def update_progress_started(started: int, total: int) -> None:
|
|
421
|
+
if task_id_holder["task_id"] is not None:
|
|
422
|
+
progress.update(task_id_holder["task_id"], completed=started)
|
|
423
|
+
|
|
424
|
+
original_callback = getattr(self.hook_manager, "_progress_callback", None)
|
|
425
|
+
original_start_callback = getattr(
|
|
426
|
+
self.hook_manager, "_progress_start_callback", None
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
self.hook_manager._progress_callback = update_progress
|
|
430
|
+
self.hook_manager._progress_start_callback = update_progress_started
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
"update": update_progress,
|
|
434
|
+
"update_started": update_progress_started,
|
|
435
|
+
"original": original_callback,
|
|
436
|
+
"original_started": original_start_callback,
|
|
437
|
+
"task_id_holder": task_id_holder,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
def _run_hooks_with_progress(
|
|
441
|
+
self,
|
|
442
|
+
suite_name: str,
|
|
443
|
+
hook_runner: t.Callable[[], list[HookResult]],
|
|
444
|
+
progress: Progress,
|
|
445
|
+
hook_count: int,
|
|
446
|
+
attempt: int,
|
|
447
|
+
callbacks: dict[str, t.Any],
|
|
448
|
+
) -> float | None:
|
|
449
|
+
"""Run hooks with progress tracking, return elapsed time or None on error."""
|
|
450
|
+
try:
|
|
451
|
+
with progress:
|
|
452
|
+
task_id = progress.add_task(
|
|
453
|
+
f"Running {suite_name} hooks:",
|
|
454
|
+
total=hook_count,
|
|
455
|
+
)
|
|
456
|
+
callbacks["task_id_holder"]["task_id"] = task_id
|
|
457
|
+
|
|
458
|
+
import time
|
|
459
|
+
|
|
460
|
+
start_time = time.time()
|
|
461
|
+
hook_results = hook_runner()
|
|
462
|
+
self._last_hook_results = hook_results
|
|
463
|
+
elapsed_time = time.time() - start_time
|
|
464
|
+
|
|
465
|
+
return elapsed_time
|
|
466
|
+
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
self._handle_hook_execution_error(suite_name, exc, attempt)
|
|
469
|
+
return None
|
|
470
|
+
finally:
|
|
471
|
+
self._restore_progress_callbacks(callbacks)
|
|
472
|
+
|
|
473
|
+
def _handle_hook_execution_error(
|
|
474
|
+
self, suite_name: str, exc: Exception, attempt: int
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Handle errors during hook execution."""
|
|
477
|
+
self.console.print(
|
|
478
|
+
f"[red]❌[/red] {suite_name.title()} hooks encountered an unexpected error: {exc}"
|
|
479
|
+
)
|
|
480
|
+
self.logger.exception(
|
|
481
|
+
"Hook execution raised exception",
|
|
482
|
+
extra={"suite": suite_name, "attempt": attempt},
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def _restore_progress_callbacks(self, callbacks: dict[str, t.Any]) -> None:
|
|
486
|
+
"""Restore original progress callbacks."""
|
|
487
|
+
self.hook_manager._progress_callback = callbacks["original"]
|
|
488
|
+
self.hook_manager._progress_start_callback = callbacks["original_started"]
|
|
489
|
+
|
|
490
|
+
def _process_hook_results(
|
|
491
|
+
self, suite_name: str, elapsed_time: float, attempt: int
|
|
492
|
+
) -> bool:
|
|
493
|
+
"""Process hook results and determine success."""
|
|
494
|
+
summary = self.hook_manager.get_hook_summary(
|
|
495
|
+
self._last_hook_results, elapsed_time=elapsed_time
|
|
496
|
+
)
|
|
497
|
+
self._last_hook_summary = summary
|
|
498
|
+
self._report_hook_results(suite_name, self._last_hook_results, summary, attempt)
|
|
499
|
+
|
|
500
|
+
if summary.get("failed", 0) == 0 == summary.get("errors", 0):
|
|
501
|
+
return True
|
|
502
|
+
|
|
503
|
+
self.logger.warning(
|
|
504
|
+
"Hook suite reported failures",
|
|
505
|
+
extra={
|
|
506
|
+
"suite": suite_name,
|
|
507
|
+
"attempt": attempt,
|
|
508
|
+
"failed": summary.get("failed", 0),
|
|
509
|
+
"errors": summary.get("errors", 0),
|
|
510
|
+
},
|
|
511
|
+
)
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
def _display_hook_phase_header(self, title: str, description: str) -> None:
|
|
515
|
+
sep = make_separator("-")
|
|
516
|
+
self.console.print("\n" + sep)
|
|
517
|
+
# Combine title and description into a single line with leading icon
|
|
518
|
+
pretty_title = title.title()
|
|
519
|
+
message = (
|
|
520
|
+
f"[bold bright_cyan]🔍 {pretty_title}[/bold bright_cyan][bold bright_white]"
|
|
521
|
+
f" - {description}[/bold bright_white]"
|
|
522
|
+
)
|
|
523
|
+
self.console.print(message)
|
|
524
|
+
self.console.print(sep + "\n")
|
|
525
|
+
|
|
526
|
+
def _report_hook_results(
|
|
527
|
+
self,
|
|
528
|
+
suite_name: str,
|
|
529
|
+
results: list[HookResult],
|
|
530
|
+
summary: dict[str, t.Any],
|
|
531
|
+
attempt: int,
|
|
532
|
+
) -> None:
|
|
533
|
+
total = summary.get("total", 0)
|
|
534
|
+
passed = summary.get("passed", 0)
|
|
535
|
+
failed = summary.get("failed", 0)
|
|
536
|
+
errors = summary.get("errors", 0)
|
|
537
|
+
duration = summary.get("total_duration", 0.0)
|
|
538
|
+
|
|
539
|
+
if total == 0:
|
|
540
|
+
self.console.print(
|
|
541
|
+
f"[yellow]⚠️[/yellow] No {suite_name} hooks are configured for this project."
|
|
542
|
+
)
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
base_message = (
|
|
546
|
+
f"{suite_name.title()} hooks attempt {attempt}: "
|
|
547
|
+
f"{passed}/{total} passed in {duration:.2f}s"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if failed or errors:
|
|
551
|
+
self.console.print(f"\n[red]❌[/red] {base_message}\n")
|
|
552
|
+
# Always show a results table to aid debugging when there are failures
|
|
553
|
+
self._render_hook_results_table(suite_name, results)
|
|
554
|
+
else:
|
|
555
|
+
self.console.print(f"\n[green]✅[/green] {base_message}\n")
|
|
556
|
+
self._render_hook_results_table(suite_name, results)
|
|
557
|
+
|
|
558
|
+
def _render_hook_results_table(
|
|
559
|
+
self,
|
|
560
|
+
suite_name: str,
|
|
561
|
+
results: list[HookResult],
|
|
562
|
+
) -> None:
|
|
563
|
+
if not results:
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
if self._is_plain_output():
|
|
567
|
+
self._render_plain_hook_results(suite_name, results)
|
|
568
|
+
else:
|
|
569
|
+
self._render_rich_hook_results(suite_name, results)
|
|
570
|
+
|
|
571
|
+
def _render_plain_hook_results(
|
|
572
|
+
self, suite_name: str, results: list[HookResult]
|
|
573
|
+
) -> None:
|
|
574
|
+
"""Render hook results in plain text format."""
|
|
575
|
+
self.console.print(f"{suite_name.title()} Hook Results:", highlight=False)
|
|
576
|
+
|
|
577
|
+
stats = self._calculate_hook_statistics(results)
|
|
578
|
+
hooks_to_show = stats["failed_hooks"] or results
|
|
579
|
+
|
|
580
|
+
for result in hooks_to_show:
|
|
581
|
+
self._print_plain_hook_result(result)
|
|
582
|
+
|
|
583
|
+
if not stats["failed_hooks"] and results:
|
|
584
|
+
self._print_plain_summary(stats)
|
|
585
|
+
|
|
586
|
+
self.console.print()
|
|
587
|
+
|
|
588
|
+
def _calculate_hook_statistics(self, results: list[HookResult]) -> dict[str, t.Any]:
|
|
589
|
+
"""Calculate statistics from hook results."""
|
|
590
|
+
passed_hooks = [r for r in results if r.status.lower() in {"passed", "success"}]
|
|
591
|
+
failed_hooks = [
|
|
592
|
+
r for r in results if r.status.lower() in {"failed", "error", "timeout"}
|
|
593
|
+
]
|
|
594
|
+
other_hooks = [
|
|
595
|
+
r
|
|
596
|
+
for r in results
|
|
597
|
+
if r.status.lower()
|
|
598
|
+
not in {"passed", "success", "failed", "error", "timeout"}
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
# Calculate total issues using issues_count (which may be larger than len(issues_found))
|
|
602
|
+
# Passed hooks always contribute 0 issues
|
|
603
|
+
# Config errors (is_config_error=True) are counted separately
|
|
604
|
+
total_issues = 0
|
|
605
|
+
config_errors = 0
|
|
606
|
+
for r in results:
|
|
607
|
+
if r.status == "passed":
|
|
608
|
+
continue
|
|
609
|
+
# Count config errors separately - they're not code quality issues
|
|
610
|
+
if hasattr(r, "is_config_error") and r.is_config_error:
|
|
611
|
+
config_errors += 1
|
|
612
|
+
continue
|
|
613
|
+
# Use issues_count directly (don't fall back to len(issues_found))
|
|
614
|
+
# because issues_found may contain error detail lines, not actual issues
|
|
615
|
+
if hasattr(r, "issues_count"):
|
|
616
|
+
total_issues += r.issues_count
|
|
617
|
+
elif r.issues_found:
|
|
618
|
+
# Legacy fallback for old HookResults without issues_count
|
|
619
|
+
total_issues += len(r.issues_found)
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
"total_hooks": len(results),
|
|
623
|
+
"passed_hooks": passed_hooks,
|
|
624
|
+
"failed_hooks": failed_hooks,
|
|
625
|
+
"other_hooks": other_hooks,
|
|
626
|
+
"total_passed": len(passed_hooks),
|
|
627
|
+
"total_failed": len(failed_hooks),
|
|
628
|
+
"total_other": len(other_hooks),
|
|
629
|
+
"total_issues_found": total_issues,
|
|
630
|
+
"config_errors": config_errors,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
def _print_plain_hook_result(self, result: HookResult) -> None:
|
|
634
|
+
"""Print a single hook result in plain format."""
|
|
635
|
+
name = self._strip_ansi(result.name)
|
|
636
|
+
status = result.status.upper()
|
|
637
|
+
duration = f"{result.duration:.2f}s"
|
|
638
|
+
|
|
639
|
+
# Determine issues display (matches Rich table logic)
|
|
640
|
+
if result.status == "passed":
|
|
641
|
+
issues = "0"
|
|
642
|
+
elif hasattr(result, "is_config_error") and result.is_config_error:
|
|
643
|
+
# Config/tool error - show simple symbol instead of misleading count
|
|
644
|
+
issues = "[yellow]![/yellow]"
|
|
645
|
+
else:
|
|
646
|
+
# For failed hooks with code violations, use issues_count
|
|
647
|
+
# Don't fall back to len(issues_found) - it may contain error detail lines
|
|
648
|
+
issues = str(result.issues_count if hasattr(result, "issues_count") else 0)
|
|
649
|
+
|
|
650
|
+
self.console.print(
|
|
651
|
+
f" - {name} :: {status} | {duration} | issues={issues}",
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
def _print_plain_summary(self, stats: dict[str, t.Any]) -> None:
|
|
655
|
+
"""Print summary statistics in plain format."""
|
|
656
|
+
issues_text = f"{stats['total_issues_found']} issues found"
|
|
657
|
+
if stats.get("config_errors", 0) > 0:
|
|
658
|
+
issues_text += f" ({stats['config_errors']} config)"
|
|
659
|
+
|
|
660
|
+
self.console.print(
|
|
661
|
+
f" Summary: {stats['total_passed']}/{stats['total_hooks']} hooks passed, {issues_text}",
|
|
662
|
+
highlight=False,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
def _render_rich_hook_results(
|
|
666
|
+
self, suite_name: str, results: list[HookResult]
|
|
667
|
+
) -> None:
|
|
668
|
+
"""Render hook results in Rich format."""
|
|
669
|
+
stats = self._calculate_hook_statistics(results)
|
|
670
|
+
summary_text = self._build_summary_text(stats)
|
|
671
|
+
table = self._build_results_table(results)
|
|
672
|
+
panel = self._build_results_panel(suite_name, table, summary_text)
|
|
673
|
+
|
|
674
|
+
self.console.print(panel)
|
|
675
|
+
|
|
676
|
+
# Add legend if any config errors are present
|
|
677
|
+
has_config_errors = any(
|
|
678
|
+
hasattr(r, "is_config_error") and r.is_config_error for r in results
|
|
679
|
+
)
|
|
680
|
+
if has_config_errors:
|
|
681
|
+
self.console.print(
|
|
682
|
+
" [dim][yellow]![/yellow] = Configuration or tool error (not code "
|
|
683
|
+
"issues)[/dim]"
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
self.console.print()
|
|
687
|
+
|
|
688
|
+
@staticmethod
|
|
689
|
+
def _build_summary_text(stats: dict[str, t.Any]) -> str:
|
|
690
|
+
"""Build summary text for Rich display."""
|
|
691
|
+
summary_text = (
|
|
692
|
+
f"Total: [white]{stats['total_hooks']}[/white] | Passed:"
|
|
693
|
+
f" [green]{stats['total_passed']}[/green] | Failed: [red]{stats['total_failed']}[/red]"
|
|
694
|
+
)
|
|
695
|
+
if stats["total_other"] > 0:
|
|
696
|
+
summary_text += f" | Other: [yellow]{stats['total_other']}[/yellow]"
|
|
697
|
+
|
|
698
|
+
# Show issues found with config count in parentheses if present
|
|
699
|
+
issues_text = f"[white]{stats['total_issues_found']}[/white]"
|
|
700
|
+
if stats.get("config_errors", 0) > 0:
|
|
701
|
+
issues_text += f" [dim]({stats['config_errors']} config)[/dim]"
|
|
702
|
+
summary_text += f" | Issues found: {issues_text}"
|
|
703
|
+
return summary_text
|
|
704
|
+
|
|
705
|
+
def _build_results_table(self, results: list[HookResult]) -> Table:
|
|
706
|
+
"""Build Rich table from hook results."""
|
|
707
|
+
table = Table(
|
|
708
|
+
box=box.SIMPLE,
|
|
709
|
+
header_style="bold bright_white",
|
|
710
|
+
expand=True,
|
|
711
|
+
)
|
|
712
|
+
table.add_column("Hook", style="cyan", overflow="fold", min_width=20)
|
|
713
|
+
table.add_column("Status", style="bright_white", min_width=8)
|
|
714
|
+
table.add_column("Duration", justify="right", style="magenta", min_width=10)
|
|
715
|
+
table.add_column("Issues", justify="right", style="bright_white", min_width=8)
|
|
716
|
+
|
|
717
|
+
for result in results:
|
|
718
|
+
status_style = self._status_style(result.status)
|
|
719
|
+
# Passed hooks always show 0 issues (files processed != issues found)
|
|
720
|
+
if result.status == "passed":
|
|
721
|
+
issues_display = "0"
|
|
722
|
+
elif hasattr(result, "is_config_error") and result.is_config_error:
|
|
723
|
+
# Config/tool error - show simple symbol instead of misleading count
|
|
724
|
+
# Using "!" instead of emoji to avoid width issues in terminal
|
|
725
|
+
issues_display = "[yellow]![/yellow]"
|
|
726
|
+
else:
|
|
727
|
+
# For failed hooks with code violations, use issues_count
|
|
728
|
+
# IMPORTANT: Use issues_count directly, don't fall back to len(issues_found)
|
|
729
|
+
# because issues_found may contain display messages that aren't actual issues
|
|
730
|
+
issues_display = str(
|
|
731
|
+
result.issues_count if hasattr(result, "issues_count") else 0
|
|
732
|
+
)
|
|
733
|
+
table.add_row(
|
|
734
|
+
self._strip_ansi(result.name),
|
|
735
|
+
f"[{status_style}]{result.status.upper()}[/{status_style}]",
|
|
736
|
+
f"{result.duration:.2f}s",
|
|
737
|
+
issues_display,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
return table
|
|
741
|
+
|
|
742
|
+
def _format_issues(self, issues: list[str]) -> list[dict[str, str | int | None]]:
|
|
743
|
+
"""Format hook issues into structured dictionaries."""
|
|
744
|
+
|
|
745
|
+
def _format_single_issue(issue):
|
|
746
|
+
if hasattr(issue, "file_path") and hasattr(issue, "line_number"):
|
|
747
|
+
return {
|
|
748
|
+
"file": str(getattr(issue, "file_path", "unknown")),
|
|
749
|
+
"line": getattr(issue, "line_number", 0),
|
|
750
|
+
"message": getattr(issue, "message", str(issue)),
|
|
751
|
+
"code": getattr(issue, "code", None),
|
|
752
|
+
"severity": getattr(issue, "severity", "warning"),
|
|
753
|
+
"suggestion": getattr(issue, "suggestion", None),
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"file": "unknown",
|
|
758
|
+
"line": 0,
|
|
759
|
+
"message": str(issue),
|
|
760
|
+
"code": None,
|
|
761
|
+
"severity": "warning",
|
|
762
|
+
"suggestion": None,
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return [_format_single_issue(issue) for issue in issues]
|
|
766
|
+
|
|
767
|
+
def to_json(self, results: list[HookResult], suite_name: str = "") -> dict:
|
|
768
|
+
"""Export hook results as structured JSON for automation.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
results: List of HookResult objects to export
|
|
772
|
+
suite_name: Optional suite name (fast/comprehensive)
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
Dictionary with structured results data
|
|
776
|
+
|
|
777
|
+
Example:
|
|
778
|
+
>>> json_data = coordinator.to_json(results, "comprehensive")
|
|
779
|
+
>>> print(json.dumps(json_data, indent=2))
|
|
780
|
+
"""
|
|
781
|
+
return {
|
|
782
|
+
"suite": suite_name,
|
|
783
|
+
"summary": self._calculate_hook_statistics(results),
|
|
784
|
+
"hooks": [
|
|
785
|
+
{
|
|
786
|
+
"name": result.name,
|
|
787
|
+
"status": result.status,
|
|
788
|
+
"duration": round(result.duration, 2),
|
|
789
|
+
"issues_count": len(result.issues_found)
|
|
790
|
+
if result.issues_found
|
|
791
|
+
else 0,
|
|
792
|
+
"issues": self._format_issues(result.issues_found)
|
|
793
|
+
if result.issues_found
|
|
794
|
+
else [],
|
|
795
|
+
}
|
|
796
|
+
for result in results
|
|
797
|
+
],
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
def _build_results_panel(
|
|
801
|
+
self, suite_name: str, table: Table, summary_text: str
|
|
802
|
+
) -> Panel:
|
|
803
|
+
"""Build Rich panel containing results table."""
|
|
804
|
+
return Panel(
|
|
805
|
+
table,
|
|
806
|
+
title=f"[bold]{suite_name.title()} Hook Results[/bold]",
|
|
807
|
+
subtitle=summary_text,
|
|
808
|
+
border_style="cyan" if suite_name == "fast" else "magenta",
|
|
809
|
+
padding=(0, 1),
|
|
810
|
+
width=get_console_width(),
|
|
811
|
+
expand=True,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
def _format_failing_hooks(
|
|
815
|
+
self, suite_name: str, results: list[HookResult]
|
|
816
|
+
) -> list[HookResult]:
|
|
817
|
+
"""Get list of failing hooks and print header.
|
|
818
|
+
|
|
819
|
+
Returns:
|
|
820
|
+
List of failing hook results
|
|
821
|
+
"""
|
|
822
|
+
failing = [
|
|
823
|
+
result
|
|
824
|
+
for result in results
|
|
825
|
+
if result.status.lower() in {"failed", "error", "timeout"}
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
if failing:
|
|
829
|
+
self.console.print(
|
|
830
|
+
f"[red]Details for failing {suite_name} hooks:[/red]", highlight=False
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
return failing
|
|
834
|
+
|
|
835
|
+
def _display_issue_details(self, result: HookResult) -> None:
|
|
836
|
+
"""Display specific issue details if found."""
|
|
837
|
+
if not result.issues_found:
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
for issue in result.issues_found:
|
|
841
|
+
self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
|
|
842
|
+
|
|
843
|
+
def _display_timeout_info(self, result: HookResult) -> None:
|
|
844
|
+
"""Display timeout information."""
|
|
845
|
+
if result.is_timeout:
|
|
846
|
+
self.console.print(
|
|
847
|
+
" - Hook timed out during execution", highlight=False
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
def _display_exit_code_info(self, result: HookResult) -> None:
|
|
851
|
+
"""Display exit code with helpful context."""
|
|
852
|
+
if result.exit_code is not None and result.exit_code != 0:
|
|
853
|
+
exit_msg = f"Exit code: {result.exit_code}"
|
|
854
|
+
# Add helpful context for common exit codes
|
|
855
|
+
if result.exit_code == 137:
|
|
856
|
+
exit_msg += " (killed - possibly timeout or out of memory)"
|
|
857
|
+
elif result.exit_code == 139:
|
|
858
|
+
exit_msg += " (segmentation fault)"
|
|
859
|
+
elif result.exit_code in {126, 127}:
|
|
860
|
+
exit_msg += " (command not found or not executable)"
|
|
861
|
+
self.console.print(f" - {exit_msg}", highlight=False)
|
|
862
|
+
|
|
863
|
+
def _display_error_message(self, result: HookResult) -> None:
|
|
864
|
+
"""Display error message preview."""
|
|
865
|
+
if result.error_message:
|
|
866
|
+
# Show first line or first 200 chars of error
|
|
867
|
+
error_preview = result.error_message.split("\n")[0][:200]
|
|
868
|
+
self.console.print(f" - Error: {error_preview}", highlight=False)
|
|
869
|
+
|
|
870
|
+
def _display_generic_failure(self, result: HookResult) -> None:
|
|
871
|
+
"""Display generic failure message if no specific details available."""
|
|
872
|
+
if not result.is_timeout and not result.exit_code and not result.error_message:
|
|
873
|
+
self.console.print(
|
|
874
|
+
" - Hook failed with no detailed error information",
|
|
875
|
+
highlight=False,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
def _display_hook_failures(
|
|
879
|
+
self,
|
|
880
|
+
suite_name: str,
|
|
881
|
+
results: list[HookResult],
|
|
882
|
+
options: OptionsProtocol,
|
|
883
|
+
) -> None:
|
|
884
|
+
# Show detailed failures if --verbose or --ai-debug flag is set
|
|
885
|
+
if not (options.verbose or getattr(options, "ai_debug", False)):
|
|
886
|
+
return
|
|
887
|
+
|
|
888
|
+
failing = self._format_failing_hooks(suite_name, results)
|
|
889
|
+
if not failing:
|
|
890
|
+
return
|
|
891
|
+
|
|
892
|
+
# Process each failing hook
|
|
893
|
+
for result in failing:
|
|
894
|
+
self._print_single_hook_failure(result)
|
|
895
|
+
|
|
896
|
+
self.console.print()
|
|
897
|
+
|
|
898
|
+
def _print_single_hook_failure(self, result: HookResult) -> None:
|
|
899
|
+
"""Print details of a single hook failure."""
|
|
900
|
+
self.console.print(
|
|
901
|
+
f" - [red]{self._strip_ansi(result.name)}[/red] ({result.status})",
|
|
902
|
+
highlight=False,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
if result.issues_found:
|
|
906
|
+
self._print_hook_issues(result)
|
|
907
|
+
else:
|
|
908
|
+
self._display_failure_reasons(result)
|
|
909
|
+
|
|
910
|
+
def _print_hook_issues(self, result: HookResult) -> None:
|
|
911
|
+
"""Print issues found for a hook."""
|
|
912
|
+
# Type assertion: issues_found is never None after __post_init__
|
|
913
|
+
assert result.issues_found is not None
|
|
914
|
+
for issue in result.issues_found:
|
|
915
|
+
# Show the issue with consistent formatting
|
|
916
|
+
self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
|
|
917
|
+
|
|
918
|
+
def _display_failure_reasons(self, result: HookResult) -> None:
|
|
919
|
+
"""Display reasons why a hook failed."""
|
|
920
|
+
self._display_timeout_info(result)
|
|
921
|
+
self._display_exit_code_info(result)
|
|
922
|
+
self._display_error_message(result)
|
|
923
|
+
self._display_generic_failure(result)
|
|
924
|
+
|
|
925
|
+
def _display_cleaning_header(self) -> None:
|
|
926
|
+
sep = make_separator("-")
|
|
927
|
+
self.console.print("\n" + sep)
|
|
928
|
+
self.console.print("[bold bright_green]🧹 CLEANING[/bold bright_green]")
|
|
929
|
+
self.console.print(sep + "\n")
|
|
930
|
+
|
|
931
|
+
def _execute_cleaning_process(self) -> bool:
|
|
932
|
+
py_files = list(self.pkg_path.rglob("*.py"))
|
|
933
|
+
if not py_files:
|
|
934
|
+
return self._handle_no_files_to_clean()
|
|
935
|
+
|
|
936
|
+
cleaned_files = self._clean_python_files(py_files)
|
|
937
|
+
self._report_cleaning_results(cleaned_files)
|
|
938
|
+
return True
|
|
939
|
+
|
|
940
|
+
def _handle_no_files_to_clean(self) -> bool:
|
|
941
|
+
self.console.print("No Python files found to clean")
|
|
942
|
+
self.session.complete_task("cleaning", "No files to clean")
|
|
943
|
+
return True
|
|
944
|
+
|
|
945
|
+
def _clean_python_files(self, files: list[Path]) -> list[str]:
|
|
946
|
+
cleaned_files = []
|
|
947
|
+
for file in files:
|
|
948
|
+
if self.code_cleaner.should_process_file(file):
|
|
949
|
+
result = self.code_cleaner.clean_file(file)
|
|
950
|
+
if result.success:
|
|
951
|
+
cleaned_files.append(str(file))
|
|
952
|
+
return cleaned_files
|
|
953
|
+
|
|
954
|
+
def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
|
|
955
|
+
if cleaned_files:
|
|
956
|
+
self.console.print(f"Cleaned {len(cleaned_files)} files")
|
|
957
|
+
self.session.complete_task(
|
|
958
|
+
"cleaning", f"Cleaned {len(cleaned_files)} files"
|
|
959
|
+
)
|
|
960
|
+
else:
|
|
961
|
+
self.console.print("No cleaning needed for any files")
|
|
962
|
+
self.session.complete_task("cleaning", "No cleaning needed")
|
|
963
|
+
|
|
964
|
+
@staticmethod
|
|
965
|
+
def _determine_version_type(options: OptionsProtocol) -> str | None:
|
|
966
|
+
return options.publish or options.all or options.bump
|
|
967
|
+
|
|
968
|
+
def _execute_publishing_workflow(
|
|
969
|
+
self, options: OptionsProtocol, version_type: str
|
|
970
|
+
) -> bool:
|
|
971
|
+
# Store reference to current HEAD to allow rollback if needed
|
|
972
|
+
original_head = (
|
|
973
|
+
self.git_service.get_current_commit_hash()
|
|
974
|
+
if hasattr(self.git_service, "get_current_commit_hash")
|
|
975
|
+
else None
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
# STAGE 0: Pre-publish commit if needed
|
|
979
|
+
if not self._handle_pre_publish_commit(options):
|
|
980
|
+
return False
|
|
981
|
+
|
|
982
|
+
# STAGE 1: Version bump
|
|
983
|
+
new_version = self._perform_version_bump(version_type)
|
|
984
|
+
if not new_version:
|
|
985
|
+
return False
|
|
986
|
+
|
|
987
|
+
# STAGE 2: Commit, tag, and push changes
|
|
988
|
+
current_commit_hash = self._commit_version_changes(new_version)
|
|
989
|
+
if not current_commit_hash:
|
|
990
|
+
return False
|
|
991
|
+
|
|
992
|
+
# STAGE 3: Publish to PyPI
|
|
993
|
+
if not self._publish_to_pypi(
|
|
994
|
+
options, new_version, original_head, current_commit_hash
|
|
995
|
+
):
|
|
996
|
+
return False
|
|
997
|
+
|
|
998
|
+
# Finalize publishing
|
|
999
|
+
self._finalize_publishing(options, new_version)
|
|
1000
|
+
|
|
1001
|
+
self.session.complete_task("publishing", f"Published version {new_version}")
|
|
1002
|
+
return True
|
|
1003
|
+
|
|
1004
|
+
def _handle_pre_publish_commit(self, options: OptionsProtocol) -> bool:
|
|
1005
|
+
"""Handle committing existing changes before version bump if needed."""
|
|
1006
|
+
if not options.commit:
|
|
1007
|
+
return True
|
|
1008
|
+
|
|
1009
|
+
existing_changes = self.git_service.get_changed_files()
|
|
1010
|
+
if not existing_changes:
|
|
1011
|
+
return True
|
|
1012
|
+
|
|
1013
|
+
self._display_commit_push_header()
|
|
1014
|
+
self.console.print(
|
|
1015
|
+
"[cyan]ℹ️[/cyan] Committing existing changes before version bump..."
|
|
1016
|
+
)
|
|
1017
|
+
commit_message = self._get_commit_message(existing_changes, options)
|
|
1018
|
+
if not self._execute_commit_and_push(existing_changes, commit_message):
|
|
1019
|
+
self.session.fail_task("publishing", "Failed to commit pre-publish changes")
|
|
1020
|
+
return False
|
|
1021
|
+
self.console.print(
|
|
1022
|
+
"[green]✅[/green] Pre-publish changes committed and pushed\n"
|
|
1023
|
+
)
|
|
1024
|
+
return True
|
|
1025
|
+
|
|
1026
|
+
def _perform_version_bump(self, version_type: str) -> str | None:
|
|
1027
|
+
"""Perform the version bump operation."""
|
|
1028
|
+
self._display_version_bump_header()
|
|
1029
|
+
|
|
1030
|
+
new_version = self.publish_manager.bump_version(version_type)
|
|
1031
|
+
if not new_version:
|
|
1032
|
+
self.session.fail_task("publishing", "Version bumping failed")
|
|
1033
|
+
return None
|
|
1034
|
+
|
|
1035
|
+
self.console.print(f"[green]✅[/green] Version bumped to {new_version}")
|
|
1036
|
+
self.console.print(
|
|
1037
|
+
f"[green]✅[/green] Changelog updated for version {new_version}"
|
|
1038
|
+
)
|
|
1039
|
+
return new_version
|
|
1040
|
+
|
|
1041
|
+
def _commit_version_changes(self, new_version: str) -> str | None:
|
|
1042
|
+
"""Commit the version changes to git."""
|
|
1043
|
+
self._display_commit_push_header()
|
|
1044
|
+
|
|
1045
|
+
# Stage changes
|
|
1046
|
+
changed_files = self.git_service.get_changed_files()
|
|
1047
|
+
if not changed_files:
|
|
1048
|
+
self.console.print("[yellow]⚠️[/yellow] No changes to stage")
|
|
1049
|
+
self.session.fail_task("publishing", "No changes to commit")
|
|
1050
|
+
return None
|
|
1051
|
+
|
|
1052
|
+
if not self.git_service.add_files(changed_files):
|
|
1053
|
+
self.session.fail_task("publishing", "Failed to stage files")
|
|
1054
|
+
return None
|
|
1055
|
+
self.console.print(f"[green]✅[/green] Staged {len(changed_files)} files")
|
|
1056
|
+
|
|
1057
|
+
# Commit
|
|
1058
|
+
commit_message = f"chore: bump version to {new_version}"
|
|
1059
|
+
if not self.git_service.commit(commit_message):
|
|
1060
|
+
self.session.fail_task("publishing", "Failed to commit changes")
|
|
1061
|
+
return None
|
|
1062
|
+
current_commit_hash = (
|
|
1063
|
+
self.git_service.get_current_commit_hash()
|
|
1064
|
+
if hasattr(self.git_service, "get_current_commit_hash")
|
|
1065
|
+
else None
|
|
1066
|
+
)
|
|
1067
|
+
return current_commit_hash
|
|
1068
|
+
|
|
1069
|
+
def _publish_to_pypi(
|
|
1070
|
+
self,
|
|
1071
|
+
options: OptionsProtocol,
|
|
1072
|
+
new_version: str,
|
|
1073
|
+
original_head: str | None,
|
|
1074
|
+
current_commit_hash: str | None,
|
|
1075
|
+
) -> bool:
|
|
1076
|
+
"""Publish the package to PyPI."""
|
|
1077
|
+
self._display_publish_header()
|
|
1078
|
+
|
|
1079
|
+
# Build and publish package
|
|
1080
|
+
if not self.publish_manager.publish_package():
|
|
1081
|
+
self.session.fail_task("publishing", "Package publishing failed")
|
|
1082
|
+
# Attempt to rollback the version bump commit if publishing fails
|
|
1083
|
+
if current_commit_hash and original_head:
|
|
1084
|
+
self._attempt_rollback_version_bump(original_head, current_commit_hash)
|
|
1085
|
+
return False
|
|
1086
|
+
return True
|
|
1087
|
+
|
|
1088
|
+
def _finalize_publishing(self, options: OptionsProtocol, new_version: str) -> None:
|
|
1089
|
+
"""Finalize the publishing process after successful PyPI publishing."""
|
|
1090
|
+
# Create git tag and push only after successful PyPI publishing
|
|
1091
|
+
if not options.no_git_tags:
|
|
1092
|
+
if not self.publish_manager.create_git_tag_local(new_version):
|
|
1093
|
+
self.console.print(
|
|
1094
|
+
f"[yellow]⚠️[/yellow] Failed to create git tag v{new_version}"
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
# Push commit and tag together in single operation only after successful PyPI publishing
|
|
1098
|
+
if not self.git_service.push_with_tags():
|
|
1099
|
+
self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
|
|
1100
|
+
# Not failing the whole workflow for a push failure
|
|
1101
|
+
|
|
1102
|
+
def _attempt_rollback_version_bump(
|
|
1103
|
+
self, original_head: str, current_commit_hash: str
|
|
1104
|
+
) -> bool:
|
|
1105
|
+
"""Attempt to undo the version bump commit if publishing fails."""
|
|
1106
|
+
try:
|
|
1107
|
+
self.console.print(
|
|
1108
|
+
"[yellow]🔄 Attempting to rollback version bump commit...[/yellow]"
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
# Reset to the original HEAD (before version bump commit)
|
|
1112
|
+
result = self.git_service.reset_hard(original_head)
|
|
1113
|
+
|
|
1114
|
+
if result:
|
|
1115
|
+
self.console.print(
|
|
1116
|
+
f"[green]✅ Version bump commit ({current_commit_hash[:8]}...) reverted[/green]"
|
|
1117
|
+
)
|
|
1118
|
+
return True
|
|
1119
|
+
else:
|
|
1120
|
+
self.console.print("[red]❌ Failed to revert version bump commit[/red]")
|
|
1121
|
+
return False
|
|
1122
|
+
except Exception as e:
|
|
1123
|
+
self.console.print(f"[red]❌ Error during version rollback: {e}[/red]")
|
|
1124
|
+
return False
|
|
1125
|
+
|
|
1126
|
+
def _display_version_bump_header(self) -> None:
|
|
1127
|
+
sep = make_separator("-")
|
|
1128
|
+
self.console.print("\n" + sep)
|
|
1129
|
+
self.console.print("[bold bright_cyan]📝 VERSION BUMP[/bold bright_cyan]")
|
|
1130
|
+
self.console.print(sep + "\n")
|
|
1131
|
+
|
|
1132
|
+
def _display_commit_push_header(self) -> None:
|
|
1133
|
+
sep = make_separator("-")
|
|
1134
|
+
self.console.print("\n" + sep)
|
|
1135
|
+
self.console.print("[bold bright_blue]📦 COMMIT & PUSH[/bold bright_blue]")
|
|
1136
|
+
self.console.print(sep + "\n")
|
|
1137
|
+
|
|
1138
|
+
def _display_publish_header(self) -> None:
|
|
1139
|
+
sep = make_separator("-")
|
|
1140
|
+
self.console.print("\n" + sep)
|
|
1141
|
+
self.console.print("[bold bright_green]🚀 PUBLISH TO PYPI[/bold bright_green]")
|
|
1142
|
+
self.console.print(sep + "\n")
|
|
1143
|
+
|
|
1144
|
+
def _handle_no_changes_to_commit(self) -> bool:
|
|
1145
|
+
self.console.print("No changes to commit")
|
|
1146
|
+
self.session.complete_task("commit", "No changes to commit")
|
|
1147
|
+
return True
|
|
1148
|
+
|
|
1149
|
+
def _get_commit_message(
|
|
1150
|
+
self, changed_files: list[str], options: OptionsProtocol
|
|
1151
|
+
) -> str:
|
|
1152
|
+
suggestions = self.git_service.get_commit_message_suggestions(changed_files)
|
|
1153
|
+
if not suggestions:
|
|
1154
|
+
return "Update project files"
|
|
1155
|
+
|
|
1156
|
+
if not options.interactive:
|
|
1157
|
+
return suggestions[0]
|
|
1158
|
+
|
|
1159
|
+
return self._interactive_commit_message_selection(suggestions)
|
|
1160
|
+
|
|
1161
|
+
def _interactive_commit_message_selection(self, suggestions: list[str]) -> str:
|
|
1162
|
+
self._display_commit_suggestions(suggestions)
|
|
1163
|
+
choice = self.console.input(
|
|
1164
|
+
"\nEnter number, custom message, or press Enter for default: "
|
|
1165
|
+
).strip()
|
|
1166
|
+
return self._process_commit_choice(choice, suggestions)
|
|
1167
|
+
|
|
1168
|
+
def _display_commit_suggestions(self, suggestions: list[str]) -> None:
|
|
1169
|
+
self.console.print("\n[bold]Commit message suggestions:[/bold]")
|
|
1170
|
+
for i, suggestion in enumerate(suggestions, 1):
|
|
1171
|
+
self.console.print(f" [cyan]{i}[/cyan]: {suggestion}")
|
|
1172
|
+
|
|
1173
|
+
@staticmethod
|
|
1174
|
+
def _process_commit_choice(choice: str, suggestions: list[str]) -> str:
|
|
1175
|
+
if not choice:
|
|
1176
|
+
return suggestions[0]
|
|
1177
|
+
if choice.isdigit() and 1 <= int(choice) <= len(suggestions):
|
|
1178
|
+
return suggestions[int(choice) - 1]
|
|
1179
|
+
return choice
|
|
1180
|
+
|
|
1181
|
+
def _execute_commit_and_push(
|
|
1182
|
+
self, changed_files: list[str], commit_message: str
|
|
1183
|
+
) -> bool:
|
|
1184
|
+
if not self.git_service.add_files(changed_files):
|
|
1185
|
+
self.session.fail_task("commit", "Failed to add files to git")
|
|
1186
|
+
return False
|
|
1187
|
+
if not self.git_service.commit(commit_message):
|
|
1188
|
+
self.session.fail_task("commit", "Failed to commit files")
|
|
1189
|
+
return False
|
|
1190
|
+
if not self.git_service.push():
|
|
1191
|
+
self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
|
|
1192
|
+
# Not failing the whole workflow for a push failure
|
|
1193
|
+
self.session.complete_task("commit", "Committed and pushed changes")
|
|
1194
|
+
return True
|
|
1195
|
+
|
|
1196
|
+
@staticmethod
|
|
1197
|
+
def _format_hook_summary(summary: dict[str, t.Any]) -> str:
|
|
1198
|
+
if not summary:
|
|
1199
|
+
return "No hooks executed"
|
|
1200
|
+
|
|
1201
|
+
total = summary.get("total", 0)
|
|
1202
|
+
passed = summary.get("passed", 0)
|
|
1203
|
+
failed = summary.get("failed", 0)
|
|
1204
|
+
errors = summary.get("errors", 0)
|
|
1205
|
+
duration = summary.get("total_duration", 0.0)
|
|
1206
|
+
|
|
1207
|
+
parts = [f"{passed}/{total} passed"]
|
|
1208
|
+
if failed:
|
|
1209
|
+
parts.append(f"{failed} failed")
|
|
1210
|
+
if errors:
|
|
1211
|
+
parts.append(f"{errors} errors")
|
|
1212
|
+
|
|
1213
|
+
summary_str = ", ".join(parts)
|
|
1214
|
+
return f"{summary_str} in {duration:.2f}s"
|
|
1215
|
+
|
|
1216
|
+
@staticmethod
|
|
1217
|
+
def _status_style(status: str) -> str:
|
|
1218
|
+
normalized = status.lower()
|
|
1219
|
+
if normalized == "passed":
|
|
1220
|
+
return "green"
|
|
1221
|
+
if normalized in {"failed", "error"}:
|
|
1222
|
+
return "red"
|
|
1223
|
+
if normalized == "timeout":
|
|
1224
|
+
return "yellow"
|
|
1225
|
+
return "bright_white"
|
|
1226
|
+
|
|
1227
|
+
# (All printing is handled by acb.console.Console which supports robust I/O.)
|