crackerjack 0.37.9__py3-none-any.whl → 0.45.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crackerjack/README.md +19 -0
- crackerjack/__init__.py +30 -1
- crackerjack/__main__.py +342 -1263
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +27 -5
- crackerjack/adapters/_output_paths.py +167 -0
- crackerjack/adapters/_qa_adapter_base.py +309 -0
- crackerjack/adapters/_tool_adapter_base.py +706 -0
- crackerjack/adapters/ai/README.md +65 -0
- crackerjack/adapters/ai/__init__.py +5 -0
- crackerjack/adapters/ai/claude.py +853 -0
- crackerjack/adapters/complexity/README.md +53 -0
- crackerjack/adapters/complexity/__init__.py +10 -0
- crackerjack/adapters/complexity/complexipy.py +641 -0
- crackerjack/adapters/dependency/__init__.py +22 -0
- crackerjack/adapters/dependency/pip_audit.py +418 -0
- crackerjack/adapters/format/README.md +72 -0
- crackerjack/adapters/format/__init__.py +11 -0
- crackerjack/adapters/format/mdformat.py +313 -0
- crackerjack/adapters/format/ruff.py +516 -0
- crackerjack/adapters/lint/README.md +47 -0
- crackerjack/adapters/lint/__init__.py +11 -0
- crackerjack/adapters/lint/codespell.py +273 -0
- crackerjack/adapters/lsp/README.md +49 -0
- crackerjack/adapters/lsp/__init__.py +27 -0
- crackerjack/adapters/{rust_tool_manager.py → lsp/_manager.py} +3 -3
- crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
- crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
- crackerjack/adapters/refactor/README.md +59 -0
- crackerjack/adapters/refactor/__init__.py +12 -0
- crackerjack/adapters/refactor/creosote.py +318 -0
- crackerjack/adapters/refactor/refurb.py +406 -0
- crackerjack/adapters/refactor/skylos.py +494 -0
- crackerjack/adapters/sast/README.md +132 -0
- crackerjack/adapters/sast/__init__.py +32 -0
- crackerjack/adapters/sast/_base.py +201 -0
- crackerjack/adapters/sast/bandit.py +423 -0
- crackerjack/adapters/sast/pyscn.py +405 -0
- crackerjack/adapters/sast/semgrep.py +241 -0
- crackerjack/adapters/security/README.md +111 -0
- crackerjack/adapters/security/__init__.py +17 -0
- crackerjack/adapters/security/gitleaks.py +339 -0
- crackerjack/adapters/type/README.md +52 -0
- crackerjack/adapters/type/__init__.py +12 -0
- crackerjack/adapters/type/pyrefly.py +402 -0
- crackerjack/adapters/type/ty.py +402 -0
- crackerjack/adapters/type/zuban.py +522 -0
- crackerjack/adapters/utility/README.md +51 -0
- crackerjack/adapters/utility/__init__.py +10 -0
- crackerjack/adapters/utility/checks.py +884 -0
- crackerjack/agents/README.md +264 -0
- crackerjack/agents/__init__.py +40 -12
- crackerjack/agents/base.py +1 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +49 -53
- crackerjack/agents/dry_agent.py +187 -3
- crackerjack/agents/enhanced_coordinator.py +279 -0
- crackerjack/agents/enhanced_proactive_agent.py +185 -0
- crackerjack/agents/error_middleware.py +53 -0
- crackerjack/agents/formatting_agent.py +6 -8
- crackerjack/agents/helpers/__init__.py +9 -0
- crackerjack/agents/helpers/performance/__init__.py +22 -0
- crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
- crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
- crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
- crackerjack/agents/helpers/refactoring/__init__.py +22 -0
- crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
- crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
- crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
- crackerjack/agents/helpers/test_creation/__init__.py +19 -0
- crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
- crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
- crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
- crackerjack/agents/performance_agent.py +121 -1152
- crackerjack/agents/refactoring_agent.py +156 -655
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +19 -1605
- crackerjack/api.py +5 -7
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +1 -1
- crackerjack/cli/cache_handlers.py +23 -18
- crackerjack/cli/cache_handlers_enhanced.py +1 -4
- crackerjack/cli/facade.py +70 -8
- crackerjack/cli/formatting.py +13 -0
- crackerjack/cli/handlers/__init__.py +85 -0
- crackerjack/cli/handlers/advanced.py +103 -0
- crackerjack/cli/handlers/ai_features.py +62 -0
- crackerjack/cli/handlers/analytics.py +479 -0
- crackerjack/cli/handlers/changelog.py +271 -0
- crackerjack/cli/handlers/config_handlers.py +16 -0
- crackerjack/cli/handlers/coverage.py +84 -0
- crackerjack/cli/handlers/documentation.py +280 -0
- crackerjack/cli/handlers/main_handlers.py +497 -0
- crackerjack/cli/handlers/monitoring.py +371 -0
- crackerjack/cli/handlers.py +249 -49
- crackerjack/cli/interactive.py +8 -5
- crackerjack/cli/options.py +203 -110
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +60 -24
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +256 -0
- crackerjack/config/global_lock_config.py +191 -54
- crackerjack/config/hooks.py +188 -16
- crackerjack/config/loader.py +239 -0
- crackerjack/config/settings.py +141 -0
- crackerjack/config/tool_commands.py +331 -0
- crackerjack/core/README.md +393 -0
- crackerjack/core/async_workflow_orchestrator.py +79 -53
- crackerjack/core/autofix_coordinator.py +22 -9
- crackerjack/core/container.py +10 -9
- crackerjack/core/enhanced_container.py +9 -9
- crackerjack/core/performance.py +1 -1
- crackerjack/core/performance_monitor.py +5 -3
- crackerjack/core/phase_coordinator.py +1018 -634
- crackerjack/core/proactive_workflow.py +3 -3
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +167 -23
- crackerjack/core/session_coordinator.py +187 -382
- crackerjack/core/timeout_manager.py +161 -44
- crackerjack/core/workflow/__init__.py +21 -0
- crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
- crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
- crackerjack/core/workflow/workflow_issue_parser.py +714 -0
- crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
- crackerjack/core/workflow/workflow_security_gates.py +400 -0
- crackerjack/core/workflow_orchestrator.py +1247 -953
- crackerjack/data/README.md +11 -0
- crackerjack/data/__init__.py +8 -0
- crackerjack/data/models.py +79 -0
- crackerjack/data/repository.py +210 -0
- crackerjack/decorators/README.md +180 -0
- crackerjack/decorators/__init__.py +35 -0
- crackerjack/decorators/error_handling.py +649 -0
- crackerjack/decorators/error_handling_decorators.py +334 -0
- crackerjack/decorators/helpers.py +58 -0
- crackerjack/decorators/patterns.py +281 -0
- crackerjack/decorators/utils.py +58 -0
- crackerjack/docs/README.md +11 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/ai_templates.py +1 -1
- crackerjack/documentation/dual_output_generator.py +11 -9
- crackerjack/documentation/reference_generator.py +104 -59
- crackerjack/dynamic_config.py +52 -61
- crackerjack/errors.py +1 -1
- crackerjack/events/README.md +11 -0
- crackerjack/events/__init__.py +16 -0
- crackerjack/events/telemetry.py +175 -0
- crackerjack/events/workflow_bus.py +346 -0
- crackerjack/exceptions/README.md +301 -0
- crackerjack/exceptions/__init__.py +5 -0
- crackerjack/exceptions/config.py +4 -0
- crackerjack/exceptions/tool_execution_error.py +245 -0
- crackerjack/executors/README.md +591 -0
- crackerjack/executors/__init__.py +2 -0
- crackerjack/executors/async_hook_executor.py +539 -77
- crackerjack/executors/cached_hook_executor.py +3 -3
- crackerjack/executors/hook_executor.py +967 -102
- crackerjack/executors/hook_lock_manager.py +31 -22
- crackerjack/executors/individual_hook_executor.py +66 -32
- crackerjack/executors/lsp_aware_hook_executor.py +136 -57
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +23 -7
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +8 -9
- crackerjack/intelligence/README.md +557 -0
- crackerjack/interactive.py +37 -10
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/async_hook_manager.py +41 -57
- crackerjack/managers/hook_manager.py +449 -79
- crackerjack/managers/publish_manager.py +81 -36
- crackerjack/managers/test_command_builder.py +290 -12
- crackerjack/managers/test_executor.py +93 -8
- crackerjack/managers/test_manager.py +1082 -75
- crackerjack/managers/test_progress.py +118 -26
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/cache.py +25 -2
- crackerjack/mcp/client_runner.py +35 -18
- crackerjack/mcp/context.py +9 -9
- crackerjack/mcp/dashboard.py +24 -8
- crackerjack/mcp/enhanced_progress_monitor.py +34 -23
- crackerjack/mcp/file_monitor.py +27 -6
- crackerjack/mcp/progress_components.py +45 -34
- crackerjack/mcp/progress_monitor.py +6 -9
- crackerjack/mcp/rate_limiter.py +11 -7
- crackerjack/mcp/server.py +2 -0
- crackerjack/mcp/server_core.py +187 -55
- crackerjack/mcp/service_watchdog.py +12 -9
- crackerjack/mcp/task_manager.py +2 -2
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +2 -0
- crackerjack/mcp/tools/core_tools.py +75 -52
- crackerjack/mcp/tools/execution_tools.py +87 -31
- crackerjack/mcp/tools/intelligence_tools.py +2 -2
- crackerjack/mcp/tools/proactive_tools.py +1 -1
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +180 -132
- crackerjack/mcp/tools/workflow_executor.py +87 -46
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/app.py +11 -1
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +27 -4
- crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
- crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
- crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
- crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
- crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
- crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
- crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
- crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
- crackerjack/mcp/websocket/monitoring/factory.py +109 -0
- crackerjack/mcp/websocket/monitoring/filters.py +10 -0
- crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
- crackerjack/mcp/websocket/monitoring/models.py +90 -0
- crackerjack/mcp/websocket/monitoring/utils.py +171 -0
- crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
- crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
- crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
- crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
- crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
- crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
- crackerjack/mcp/websocket/monitoring_endpoints.py +16 -2930
- crackerjack/mcp/websocket/server.py +1 -3
- crackerjack/mcp/websocket/websocket_handler.py +107 -6
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +10 -1
- crackerjack/models/config.py +639 -22
- crackerjack/models/config_adapter.py +6 -6
- crackerjack/models/protocols.py +1167 -23
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +19 -3
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/ai_agent_watchdog.py +5 -4
- crackerjack/monitoring/metrics_collector.py +4 -3
- crackerjack/monitoring/regression_prevention.py +4 -3
- crackerjack/monitoring/websocket_server.py +4 -241
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +20 -67
- crackerjack/orchestration/cache/README.md +312 -0
- crackerjack/orchestration/cache/__init__.py +37 -0
- crackerjack/orchestration/cache/memory_cache.py +338 -0
- crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
- crackerjack/orchestration/config.py +297 -0
- crackerjack/orchestration/coverage_improvement.py +13 -6
- crackerjack/orchestration/execution_strategies.py +6 -6
- crackerjack/orchestration/hook_orchestrator.py +1398 -0
- crackerjack/orchestration/strategies/README.md +401 -0
- crackerjack/orchestration/strategies/__init__.py +39 -0
- crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
- crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
- crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
- crackerjack/orchestration/test_progress_streamer.py +1 -1
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/hooks.py +3 -2
- crackerjack/plugins/loader.py +3 -3
- crackerjack/plugins/managers.py +1 -1
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +8 -21
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
- crackerjack/services/ai/embeddings.py +444 -0
- crackerjack/services/ai/intelligent_commit.py +328 -0
- crackerjack/services/ai/predictive_analytics.py +510 -0
- crackerjack/services/api_extractor.py +5 -3
- crackerjack/services/bounded_status_operations.py +45 -5
- crackerjack/services/cache.py +249 -318
- crackerjack/services/changelog_automation.py +7 -3
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +83 -39
- crackerjack/services/config_merge.py +9 -6
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +13 -26
- crackerjack/services/coverage_badge_service.py +6 -4
- crackerjack/services/coverage_ratchet.py +53 -27
- crackerjack/services/debug.py +18 -7
- crackerjack/services/dependency_analyzer.py +4 -4
- crackerjack/services/dependency_monitor.py +13 -13
- crackerjack/services/documentation_generator.py +4 -2
- crackerjack/services/documentation_service.py +62 -33
- crackerjack/services/enhanced_filesystem.py +81 -27
- crackerjack/services/enterprise_optimizer.py +1 -1
- crackerjack/services/error_pattern_analyzer.py +10 -10
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +5 -7
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +80 -109
- crackerjack/services/git.py +99 -5
- crackerjack/services/health_metrics.py +4 -6
- crackerjack/services/heatmap_generator.py +12 -3
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +101 -49
- crackerjack/services/log_manager.py +2 -2
- crackerjack/services/logging.py +120 -68
- crackerjack/services/lsp_client.py +12 -12
- crackerjack/services/memory_optimizer.py +27 -22
- crackerjack/services/monitoring/README.md +30 -0
- crackerjack/services/monitoring/__init__.py +9 -0
- crackerjack/services/monitoring/dependency_monitor.py +678 -0
- crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
- crackerjack/services/monitoring/health_metrics.py +716 -0
- crackerjack/services/monitoring/metrics.py +587 -0
- crackerjack/services/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
- crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
- crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
- crackerjack/services/parallel_executor.py +166 -55
- crackerjack/services/patterns/__init__.py +142 -0
- crackerjack/services/patterns/agents.py +107 -0
- crackerjack/services/patterns/code/__init__.py +15 -0
- crackerjack/services/patterns/code/detection.py +118 -0
- crackerjack/services/patterns/code/imports.py +107 -0
- crackerjack/services/patterns/code/paths.py +159 -0
- crackerjack/services/patterns/code/performance.py +119 -0
- crackerjack/services/patterns/code/replacement.py +36 -0
- crackerjack/services/patterns/core.py +212 -0
- crackerjack/services/patterns/documentation/__init__.py +14 -0
- crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
- crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
- crackerjack/services/patterns/documentation/docstrings.py +89 -0
- crackerjack/services/patterns/formatting.py +226 -0
- crackerjack/services/patterns/operations.py +339 -0
- crackerjack/services/patterns/security/__init__.py +23 -0
- crackerjack/services/patterns/security/code_injection.py +122 -0
- crackerjack/services/patterns/security/credentials.py +190 -0
- crackerjack/services/patterns/security/path_traversal.py +221 -0
- crackerjack/services/patterns/security/unsafe_operations.py +216 -0
- crackerjack/services/patterns/templates.py +62 -0
- crackerjack/services/patterns/testing/__init__.py +18 -0
- crackerjack/services/patterns/testing/error_patterns.py +107 -0
- crackerjack/services/patterns/testing/pytest_output.py +126 -0
- crackerjack/services/patterns/tool_output/__init__.py +16 -0
- crackerjack/services/patterns/tool_output/bandit.py +72 -0
- crackerjack/services/patterns/tool_output/other.py +97 -0
- crackerjack/services/patterns/tool_output/pyright.py +67 -0
- crackerjack/services/patterns/tool_output/ruff.py +44 -0
- crackerjack/services/patterns/url_sanitization.py +114 -0
- crackerjack/services/patterns/utilities.py +42 -0
- crackerjack/services/patterns/utils.py +339 -0
- crackerjack/services/patterns/validation.py +46 -0
- crackerjack/services/patterns/versioning.py +62 -0
- crackerjack/services/predictive_analytics.py +21 -8
- crackerjack/services/profiler.py +280 -0
- crackerjack/services/quality/README.md +415 -0
- crackerjack/services/quality/__init__.py +11 -0
- crackerjack/services/quality/anomaly_detector.py +392 -0
- crackerjack/services/quality/pattern_cache.py +333 -0
- crackerjack/services/quality/pattern_detector.py +479 -0
- crackerjack/services/quality/qa_orchestrator.py +491 -0
- crackerjack/services/{quality_baseline.py → quality/quality_baseline.py} +163 -2
- crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
- crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
- crackerjack/services/regex_patterns.py +58 -2987
- crackerjack/services/regex_utils.py +55 -29
- crackerjack/services/secure_status_formatter.py +42 -15
- crackerjack/services/secure_subprocess.py +35 -2
- crackerjack/services/security.py +16 -8
- crackerjack/services/server_manager.py +40 -51
- crackerjack/services/smart_scheduling.py +46 -6
- crackerjack/services/status_authentication.py +3 -3
- crackerjack/services/thread_safe_status_collector.py +1 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +9 -5
- crackerjack/services/unified_config.py +43 -351
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +6 -4
- crackerjack/services/version_checker.py +14 -8
- crackerjack/services/zuban_lsp_service.py +5 -4
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/init.md +2 -12
- crackerjack/slash_commands/run.md +84 -50
- crackerjack/tools/README.md +11 -0
- crackerjack/tools/__init__.py +30 -0
- crackerjack/tools/_git_utils.py +105 -0
- crackerjack/tools/check_added_large_files.py +139 -0
- crackerjack/tools/check_ast.py +105 -0
- crackerjack/tools/check_json.py +103 -0
- crackerjack/tools/check_jsonschema.py +297 -0
- crackerjack/tools/check_toml.py +103 -0
- crackerjack/tools/check_yaml.py +110 -0
- crackerjack/tools/codespell_wrapper.py +72 -0
- crackerjack/tools/end_of_file_fixer.py +202 -0
- crackerjack/tools/format_json.py +128 -0
- crackerjack/tools/mdformat_wrapper.py +114 -0
- crackerjack/tools/trailing_whitespace.py +198 -0
- crackerjack/tools/validate_regex_patterns.py +7 -3
- crackerjack/ui/README.md +11 -0
- crackerjack/ui/dashboard_renderer.py +28 -0
- crackerjack/ui/templates/README.md +11 -0
- crackerjack/utils/console_utils.py +13 -0
- crackerjack/utils/dependency_guard.py +230 -0
- crackerjack/utils/retry_utils.py +275 -0
- crackerjack/workflows/README.md +590 -0
- crackerjack/workflows/__init__.py +46 -0
- crackerjack/workflows/actions.py +811 -0
- crackerjack/workflows/auto_fix.py +444 -0
- crackerjack/workflows/container_builder.py +499 -0
- crackerjack/workflows/definitions.py +443 -0
- crackerjack/workflows/engine.py +177 -0
- crackerjack/workflows/event_bridge.py +242 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack/managers/test_manager_backup.py +0 -1075
- crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
- crackerjack/mixins/__init__.py +0 -3
- crackerjack/mixins/error_handling.py +0 -145
- crackerjack/services/config.py +0 -358
- crackerjack/ui/server_panels.py +0 -125
- crackerjack-0.37.9.dist-info/RECORD +0 -231
- /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
- /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
3
|
import typing as t
|
|
4
|
+
from contextlib import suppress
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
|
-
from
|
|
8
|
+
from acb.console import Console
|
|
9
|
+
from acb.depends import Inject, depends
|
|
10
|
+
from acb.logger import Logger
|
|
8
11
|
|
|
12
|
+
from crackerjack.config import get_console_width
|
|
9
13
|
from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
|
|
10
14
|
from crackerjack.models.protocols import HookLockManagerProtocol
|
|
11
15
|
from crackerjack.models.task import HookResult
|
|
12
|
-
from crackerjack.services.logging import LoggingContext
|
|
16
|
+
from crackerjack.services.logging import LoggingContext
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
@dataclass
|
|
@@ -52,8 +56,10 @@ class AsyncHookExecutionResult:
|
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
class AsyncHookExecutor:
|
|
59
|
+
@depends.inject
|
|
55
60
|
def __init__(
|
|
56
61
|
self,
|
|
62
|
+
logger: Inject[Logger],
|
|
57
63
|
console: Console,
|
|
58
64
|
pkg_path: Path,
|
|
59
65
|
max_concurrent: int = 4,
|
|
@@ -66,9 +72,12 @@ class AsyncHookExecutor:
|
|
|
66
72
|
self.max_concurrent = max_concurrent
|
|
67
73
|
self.timeout = timeout
|
|
68
74
|
self.quiet = quiet
|
|
69
|
-
self.logger =
|
|
75
|
+
self.logger = logger
|
|
70
76
|
|
|
71
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
|
|
72
81
|
|
|
73
82
|
if hook_lock_manager is None:
|
|
74
83
|
from crackerjack.executors.hook_lock_manager import (
|
|
@@ -99,7 +108,7 @@ class AsyncHookExecutor:
|
|
|
99
108
|
max_workers=getattr(strategy, "max_workers", self.max_concurrent),
|
|
100
109
|
)
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
# Header is displayed by PhaseCoordinator; suppress here to avoid duplicates
|
|
103
112
|
|
|
104
113
|
estimated_sequential = sum(
|
|
105
114
|
getattr(hook, "timeout", 30) for hook in strategy.hooks
|
|
@@ -156,20 +165,8 @@ class AsyncHookExecutor:
|
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
def _print_strategy_header(self, strategy: HookStrategy) -> None:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
self.console.print(
|
|
162
|
-
"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running code quality checks (async)[/ bold bright_white]",
|
|
163
|
-
)
|
|
164
|
-
elif strategy.name == "comprehensive":
|
|
165
|
-
self.console.print(
|
|
166
|
-
"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running comprehensive quality checks (async)[/ bold bright_white]",
|
|
167
|
-
)
|
|
168
|
-
else:
|
|
169
|
-
self.console.print(
|
|
170
|
-
f"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running {strategy.name} hooks (async)[/ bold bright_white]",
|
|
171
|
-
)
|
|
172
|
-
self.console.print("-" * 74 + "\n")
|
|
168
|
+
# Intentionally no-op: PhaseCoordinator controls stage headers
|
|
169
|
+
return None
|
|
173
170
|
|
|
174
171
|
async def _execute_sequential(self, strategy: HookStrategy) -> list[HookResult]:
|
|
175
172
|
results: list[HookResult] = []
|
|
@@ -218,6 +215,69 @@ class AsyncHookExecutor:
|
|
|
218
215
|
|
|
219
216
|
return results
|
|
220
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
|
+
|
|
221
281
|
async def _execute_single_hook(self, hook: HookDefinition) -> HookResult:
|
|
222
282
|
async with self._semaphore:
|
|
223
283
|
if self.hook_lock_manager.requires_lock(hook.name):
|
|
@@ -257,11 +317,7 @@ class AsyncHookExecutor:
|
|
|
257
317
|
timeout=timeout_val,
|
|
258
318
|
)
|
|
259
319
|
|
|
260
|
-
repo_root = (
|
|
261
|
-
self.pkg_path.parent
|
|
262
|
-
if self.pkg_path.name == "crackerjack"
|
|
263
|
-
else self.pkg_path
|
|
264
|
-
)
|
|
320
|
+
repo_root = self._get_repo_root()
|
|
265
321
|
process = await asyncio.create_subprocess_exec(
|
|
266
322
|
*cmd,
|
|
267
323
|
cwd=repo_root,
|
|
@@ -269,68 +325,188 @@ class AsyncHookExecutor:
|
|
|
269
325
|
stderr=asyncio.subprocess.PIPE,
|
|
270
326
|
)
|
|
271
327
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
process.communicate(),
|
|
275
|
-
timeout=timeout_val,
|
|
276
|
-
)
|
|
277
|
-
except TimeoutError:
|
|
278
|
-
process.kill()
|
|
279
|
-
await process.wait()
|
|
280
|
-
duration = time.time() - start_time
|
|
281
|
-
|
|
282
|
-
self.logger.warning(
|
|
283
|
-
"Hook execution timed out",
|
|
284
|
-
hook=hook.name,
|
|
285
|
-
timeout=timeout_val,
|
|
286
|
-
duration_seconds=round(duration, 2),
|
|
287
|
-
)
|
|
328
|
+
# Track this process for cleanup
|
|
329
|
+
self._running_processes.add(process)
|
|
288
330
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
issues_found=[f"Hook timed out after {duration: .1f}s"],
|
|
295
|
-
stage=hook.stage.value,
|
|
296
|
-
)
|
|
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
|
|
297
336
|
|
|
337
|
+
# Process completed successfully
|
|
298
338
|
duration = time.time() - start_time
|
|
299
|
-
|
|
300
|
-
|
|
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,
|
|
301
366
|
)
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
)
|
|
304
395
|
|
|
305
|
-
|
|
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
|
+
)
|
|
306
408
|
|
|
307
|
-
|
|
308
|
-
|
|
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",
|
|
309
433
|
hook=hook.name,
|
|
310
|
-
status=status,
|
|
311
|
-
duration_seconds=round(duration, 2),
|
|
312
|
-
return_code=process.returncode,
|
|
313
|
-
files_processed=parsed_output.get("files_processed", 0),
|
|
314
|
-
issues_count=len(parsed_output.get("issues", [])),
|
|
315
434
|
)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
status=status,
|
|
321
|
-
duration=duration,
|
|
322
|
-
files_processed=parsed_output.get("files_processed", 0),
|
|
323
|
-
issues_found=parsed_output.get("issues", []),
|
|
324
|
-
stage=hook.stage.value,
|
|
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,
|
|
325
439
|
)
|
|
326
440
|
|
|
327
|
-
|
|
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):
|
|
328
506
|
duration = time.time() - start_time
|
|
329
|
-
self.logger.
|
|
330
|
-
"
|
|
507
|
+
self.logger.warning(
|
|
508
|
+
"Event loop closed during hook execution, returning error",
|
|
331
509
|
hook=hook.name,
|
|
332
|
-
error=str(e),
|
|
333
|
-
error_type=type(e).__name__,
|
|
334
510
|
duration_seconds=round(duration, 2),
|
|
335
511
|
)
|
|
336
512
|
return HookResult(
|
|
@@ -338,11 +514,212 @@ class AsyncHookExecutor:
|
|
|
338
514
|
name=hook.name,
|
|
339
515
|
status="error",
|
|
340
516
|
duration=duration,
|
|
341
|
-
issues_found=[
|
|
517
|
+
issues_found=["Event loop closed during execution"],
|
|
518
|
+
issues_count=1, # Error counts as 1 issue
|
|
342
519
|
stage=hook.stage.value,
|
|
520
|
+
exit_code=1,
|
|
521
|
+
error_message="Event loop closed during hook execution",
|
|
522
|
+
is_timeout=False,
|
|
343
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()
|
|
344
571
|
|
|
345
|
-
|
|
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."""
|
|
346
723
|
return {
|
|
347
724
|
"hook_id": None,
|
|
348
725
|
"exit_code": returncode,
|
|
@@ -351,13 +728,98 @@ class AsyncHookExecutor:
|
|
|
351
728
|
"raw_output": output,
|
|
352
729
|
}
|
|
353
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
|
+
|
|
354
813
|
def _display_hook_result(self, result: HookResult) -> None:
|
|
355
|
-
|
|
814
|
+
if self.quiet:
|
|
815
|
+
return
|
|
816
|
+
width = get_console_width()
|
|
817
|
+
dots = "." * max(0, (width - len(result.name)))
|
|
356
818
|
status_text = "Passed" if result.status == "passed" else "Failed"
|
|
357
819
|
status_color = "green" if result.status == "passed" else "red"
|
|
358
820
|
|
|
359
821
|
self.console.print(
|
|
360
|
-
f"{result.name}{dots}[{status_color}]{status_text}[/{status_color}]"
|
|
822
|
+
f"{result.name}{dots}[{status_color}]{status_text}[/{status_color}]"
|
|
361
823
|
)
|
|
362
824
|
|
|
363
825
|
if result.status != "passed" and result.issues_found:
|