crackerjack 0.32.0__py3-none-any.whl → 0.33.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/core/enhanced_container.py +67 -0
- crackerjack/core/phase_coordinator.py +183 -44
- crackerjack/core/workflow_orchestrator.py +459 -138
- crackerjack/managers/publish_manager.py +22 -5
- crackerjack/managers/test_command_builder.py +4 -2
- crackerjack/managers/test_manager.py +15 -4
- crackerjack/mcp/server_core.py +162 -34
- crackerjack/mcp/tools/core_tools.py +1 -1
- crackerjack/mcp/tools/execution_tools.py +8 -3
- crackerjack/mixins/__init__.py +5 -0
- crackerjack/mixins/error_handling.py +214 -0
- crackerjack/models/config.py +9 -0
- crackerjack/models/protocols.py +69 -0
- crackerjack/models/task.py +3 -0
- crackerjack/security/__init__.py +1 -1
- crackerjack/security/audit.py +92 -78
- crackerjack/services/config.py +3 -2
- crackerjack/services/config_merge.py +11 -5
- crackerjack/services/coverage_ratchet.py +22 -0
- crackerjack/services/git.py +37 -24
- crackerjack/services/initialization.py +25 -9
- crackerjack/services/memory_optimizer.py +477 -0
- crackerjack/services/parallel_executor.py +474 -0
- crackerjack/services/performance_benchmarks.py +292 -577
- crackerjack/services/performance_cache.py +443 -0
- crackerjack/services/performance_monitor.py +633 -0
- crackerjack/services/security.py +63 -0
- crackerjack/services/security_logger.py +9 -1
- crackerjack/services/terminal_utils.py +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/METADATA +2 -2
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/RECORD +34 -27
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/WHEEL +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.0.dist-info}/licenses/LICENSE +0 -0
crackerjack/models/protocols.py
CHANGED
|
@@ -144,6 +144,75 @@ class SecurityAwareHookManager(HookManager, t.Protocol):
|
|
|
144
144
|
...
|
|
145
145
|
|
|
146
146
|
|
|
147
|
+
@t.runtime_checkable
|
|
148
|
+
class CoverageRatchetProtocol(t.Protocol):
|
|
149
|
+
"""Protocol for coverage ratchet service."""
|
|
150
|
+
|
|
151
|
+
def get_baseline_coverage(self) -> float: ...
|
|
152
|
+
|
|
153
|
+
def update_baseline_coverage(self, new_coverage: float) -> bool: ...
|
|
154
|
+
|
|
155
|
+
def is_coverage_regression(self, current_coverage: float) -> bool: ...
|
|
156
|
+
|
|
157
|
+
def get_coverage_improvement_needed(self) -> float: ...
|
|
158
|
+
|
|
159
|
+
def get_status_report(self) -> dict[str, t.Any]: ...
|
|
160
|
+
|
|
161
|
+
def get_coverage_report(self) -> str | None: ...
|
|
162
|
+
|
|
163
|
+
def check_and_update_coverage(self) -> dict[str, t.Any]: ...
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@t.runtime_checkable
|
|
167
|
+
class ConfigurationServiceProtocol(t.Protocol):
|
|
168
|
+
"""Protocol for configuration service."""
|
|
169
|
+
|
|
170
|
+
def update_precommit_config(self, options: OptionsProtocol) -> bool: ...
|
|
171
|
+
|
|
172
|
+
def update_pyproject_config(self, options: OptionsProtocol) -> bool: ...
|
|
173
|
+
|
|
174
|
+
def get_temp_config_path(self) -> str | None: ...
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@t.runtime_checkable
|
|
178
|
+
class SecurityServiceProtocol(t.Protocol):
|
|
179
|
+
"""Protocol for security service."""
|
|
180
|
+
|
|
181
|
+
def validate_file_safety(self, path: str | Path) -> bool: ...
|
|
182
|
+
|
|
183
|
+
def check_hardcoded_secrets(self, content: str) -> list[dict[str, t.Any]]: ...
|
|
184
|
+
|
|
185
|
+
def is_safe_subprocess_call(self, cmd: list[str]) -> bool: ...
|
|
186
|
+
|
|
187
|
+
def create_secure_command_env(self) -> dict[str, str]: ...
|
|
188
|
+
|
|
189
|
+
def mask_tokens(self, text: str) -> str: ...
|
|
190
|
+
|
|
191
|
+
def validate_token_format(self, token: str, token_type: str) -> bool: ...
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@t.runtime_checkable
|
|
195
|
+
class InitializationServiceProtocol(t.Protocol):
|
|
196
|
+
"""Protocol for initialization service."""
|
|
197
|
+
|
|
198
|
+
def initialize_project(self, project_path: str | Path) -> bool: ...
|
|
199
|
+
|
|
200
|
+
def validate_project_structure(self) -> bool: ...
|
|
201
|
+
|
|
202
|
+
def setup_git_hooks(self) -> bool: ...
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@t.runtime_checkable
|
|
206
|
+
class UnifiedConfigurationServiceProtocol(t.Protocol):
|
|
207
|
+
"""Protocol for unified configuration service."""
|
|
208
|
+
|
|
209
|
+
def merge_configurations(self) -> dict[str, t.Any]: ...
|
|
210
|
+
|
|
211
|
+
def validate_configuration(self, config: dict[str, t.Any]) -> bool: ...
|
|
212
|
+
|
|
213
|
+
def get_merged_config(self) -> dict[str, t.Any]: ...
|
|
214
|
+
|
|
215
|
+
|
|
147
216
|
@t.runtime_checkable
|
|
148
217
|
class TestManagerProtocol(t.Protocol):
|
|
149
218
|
def run_tests(self, options: OptionsProtocol) -> bool: ...
|
crackerjack/models/task.py
CHANGED
|
@@ -56,10 +56,13 @@ class TaskStatusData:
|
|
|
56
56
|
details: str | None = None
|
|
57
57
|
error_message: str | None = None
|
|
58
58
|
files_changed: list[str] | None = None
|
|
59
|
+
hook_results: list[t.Any] | None = None
|
|
59
60
|
|
|
60
61
|
def __post_init__(self) -> None:
|
|
61
62
|
if self.files_changed is None:
|
|
62
63
|
self.files_changed = []
|
|
64
|
+
if self.hook_results is None:
|
|
65
|
+
self.hook_results = []
|
|
63
66
|
if self.start_time is not None and self.end_time is not None:
|
|
64
67
|
self.duration = self.end_time - self.start_time
|
|
65
68
|
|
crackerjack/security/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Security utilities for Crackerjack."""
|
|
1
|
+
"""Security utilities for Crackerjack."""
|
crackerjack/security/audit.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import typing as t
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from enum import Enum
|
|
6
5
|
|
|
7
6
|
from crackerjack.config.hooks import SecurityLevel
|
|
8
7
|
|
|
@@ -10,7 +9,7 @@ from crackerjack.config.hooks import SecurityLevel
|
|
|
10
9
|
@dataclass
|
|
11
10
|
class SecurityCheckResult:
|
|
12
11
|
"""Result of a security check."""
|
|
13
|
-
|
|
12
|
+
|
|
14
13
|
hook_name: str
|
|
15
14
|
security_level: SecurityLevel
|
|
16
15
|
passed: bool
|
|
@@ -18,74 +17,72 @@ class SecurityCheckResult:
|
|
|
18
17
|
details: dict[str, t.Any] | None = None
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
@dataclass
|
|
20
|
+
@dataclass
|
|
22
21
|
class SecurityAuditReport:
|
|
23
22
|
"""Comprehensive security audit report for publishing decisions."""
|
|
24
|
-
|
|
23
|
+
|
|
25
24
|
critical_failures: list[SecurityCheckResult]
|
|
26
|
-
high_failures: list[SecurityCheckResult]
|
|
25
|
+
high_failures: list[SecurityCheckResult]
|
|
27
26
|
medium_failures: list[SecurityCheckResult]
|
|
28
27
|
low_failures: list[SecurityCheckResult]
|
|
29
|
-
|
|
28
|
+
|
|
30
29
|
allows_publishing: bool
|
|
31
30
|
security_warnings: list[str]
|
|
32
31
|
recommendations: list[str]
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
@property
|
|
35
34
|
def has_critical_failures(self) -> bool:
|
|
36
35
|
"""Check if there are any critical security failures."""
|
|
37
36
|
return len(self.critical_failures) > 0
|
|
38
|
-
|
|
37
|
+
|
|
39
38
|
@property
|
|
40
39
|
def total_failures(self) -> int:
|
|
41
40
|
"""Get total number of failed checks."""
|
|
42
41
|
return (
|
|
43
|
-
len(self.critical_failures)
|
|
44
|
-
len(self.high_failures)
|
|
45
|
-
len(self.medium_failures)
|
|
46
|
-
len(self.low_failures)
|
|
42
|
+
len(self.critical_failures)
|
|
43
|
+
+ len(self.high_failures)
|
|
44
|
+
+ len(self.medium_failures)
|
|
45
|
+
+ len(self.low_failures)
|
|
47
46
|
)
|
|
48
47
|
|
|
49
48
|
|
|
50
49
|
class SecurityAuditor:
|
|
51
50
|
"""Security auditor for hook results following OWASP secure SDLC practices."""
|
|
52
|
-
|
|
51
|
+
|
|
53
52
|
# Security-critical hooks that CANNOT be bypassed for publishing
|
|
54
53
|
CRITICAL_HOOKS = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
"bandit": "Security vulnerability detection (OWASP A09)",
|
|
55
|
+
"pyright": "Type safety prevents runtime security holes (OWASP A04)",
|
|
56
|
+
"gitleaks": "Secret/credential detection (OWASP A07)",
|
|
58
57
|
}
|
|
59
|
-
|
|
58
|
+
|
|
60
59
|
# High-importance security hooks that can be bypassed with warnings
|
|
61
60
|
HIGH_SECURITY_HOOKS = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
"validate-regex-patterns": "Regex vulnerability detection",
|
|
62
|
+
"creosote": "Dependency vulnerability analysis",
|
|
63
|
+
"check-added-large-files": "Large file security analysis",
|
|
64
|
+
"uv-lock": "Dependency lock security",
|
|
66
65
|
}
|
|
67
|
-
|
|
66
|
+
|
|
68
67
|
def audit_hook_results(
|
|
69
|
-
self,
|
|
70
|
-
fast_results: list[t.Any],
|
|
71
|
-
comprehensive_results: list[t.Any]
|
|
68
|
+
self, fast_results: list[t.Any], comprehensive_results: list[t.Any]
|
|
72
69
|
) -> SecurityAuditReport:
|
|
73
70
|
"""Audit hook results and generate security report.
|
|
74
|
-
|
|
71
|
+
|
|
75
72
|
Args:
|
|
76
73
|
fast_results: Results from fast hooks
|
|
77
74
|
comprehensive_results: Results from comprehensive hooks
|
|
78
|
-
|
|
75
|
+
|
|
79
76
|
Returns:
|
|
80
77
|
SecurityAuditReport with security analysis
|
|
81
78
|
"""
|
|
82
79
|
all_results = fast_results + comprehensive_results
|
|
83
|
-
|
|
80
|
+
|
|
84
81
|
critical_failures = []
|
|
85
|
-
high_failures = []
|
|
82
|
+
high_failures = []
|
|
86
83
|
medium_failures = []
|
|
87
84
|
low_failures = []
|
|
88
|
-
|
|
85
|
+
|
|
89
86
|
for result in all_results:
|
|
90
87
|
check_result = self._analyze_hook_result(result)
|
|
91
88
|
if not check_result.passed:
|
|
@@ -97,18 +94,18 @@ class SecurityAuditor:
|
|
|
97
94
|
medium_failures.append(check_result)
|
|
98
95
|
else:
|
|
99
96
|
low_failures.append(check_result)
|
|
100
|
-
|
|
97
|
+
|
|
101
98
|
# Publishing is allowed only if no critical failures exist
|
|
102
99
|
allows_publishing = len(critical_failures) == 0
|
|
103
|
-
|
|
100
|
+
|
|
104
101
|
security_warnings = self._generate_security_warnings(
|
|
105
102
|
critical_failures, high_failures, medium_failures
|
|
106
103
|
)
|
|
107
|
-
|
|
104
|
+
|
|
108
105
|
recommendations = self._generate_security_recommendations(
|
|
109
106
|
critical_failures, high_failures, medium_failures
|
|
110
107
|
)
|
|
111
|
-
|
|
108
|
+
|
|
112
109
|
return SecurityAuditReport(
|
|
113
110
|
critical_failures=critical_failures,
|
|
114
111
|
high_failures=high_failures,
|
|
@@ -118,95 +115,112 @@ class SecurityAuditor:
|
|
|
118
115
|
security_warnings=security_warnings,
|
|
119
116
|
recommendations=recommendations,
|
|
120
117
|
)
|
|
121
|
-
|
|
118
|
+
|
|
122
119
|
def _analyze_hook_result(self, result: t.Any) -> SecurityCheckResult:
|
|
123
120
|
"""Analyze a single hook result for security implications."""
|
|
124
|
-
hook_name = getattr(result,
|
|
125
|
-
is_failed = getattr(result,
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
hook_name = getattr(result, "name", "unknown")
|
|
122
|
+
is_failed = getattr(result, "status", "unknown") in (
|
|
123
|
+
"failed",
|
|
124
|
+
"error",
|
|
125
|
+
"timeout",
|
|
126
|
+
)
|
|
127
|
+
error_message = getattr(result, "output", None) or getattr(
|
|
128
|
+
result, "error", None
|
|
129
|
+
)
|
|
130
|
+
|
|
128
131
|
# Determine security level
|
|
129
132
|
security_level = self._get_hook_security_level(hook_name)
|
|
130
|
-
|
|
133
|
+
|
|
131
134
|
return SecurityCheckResult(
|
|
132
135
|
hook_name=hook_name,
|
|
133
136
|
security_level=security_level,
|
|
134
137
|
passed=not is_failed,
|
|
135
138
|
error_message=error_message,
|
|
136
|
-
details={
|
|
139
|
+
details={"status": getattr(result, "status", "unknown")},
|
|
137
140
|
)
|
|
138
|
-
|
|
141
|
+
|
|
139
142
|
def _get_hook_security_level(self, hook_name: str) -> SecurityLevel:
|
|
140
143
|
"""Get security level for a hook name."""
|
|
141
144
|
hook_name_lower = hook_name.lower()
|
|
142
|
-
|
|
145
|
+
|
|
143
146
|
if hook_name_lower in [name.lower() for name in self.CRITICAL_HOOKS]:
|
|
144
147
|
return SecurityLevel.CRITICAL
|
|
145
148
|
elif hook_name_lower in [name.lower() for name in self.HIGH_SECURITY_HOOKS]:
|
|
146
149
|
return SecurityLevel.HIGH
|
|
147
|
-
elif hook_name_lower in
|
|
150
|
+
elif hook_name_lower in ("ruff-check", "vulture", "refurb", "complexipy"):
|
|
148
151
|
return SecurityLevel.MEDIUM
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
return SecurityLevel.LOW
|
|
153
|
+
|
|
152
154
|
def _generate_security_warnings(
|
|
153
|
-
self,
|
|
155
|
+
self,
|
|
154
156
|
critical: list[SecurityCheckResult],
|
|
155
|
-
high: list[SecurityCheckResult],
|
|
156
|
-
medium: list[SecurityCheckResult]
|
|
157
|
+
high: list[SecurityCheckResult],
|
|
158
|
+
medium: list[SecurityCheckResult],
|
|
157
159
|
) -> list[str]:
|
|
158
160
|
"""Generate security warnings based on failed checks."""
|
|
159
161
|
warnings = []
|
|
160
|
-
|
|
162
|
+
|
|
161
163
|
if critical:
|
|
162
164
|
warnings.append(
|
|
163
165
|
f"🔒 CRITICAL: {len(critical)} security-critical checks failed - publishing BLOCKED"
|
|
164
166
|
)
|
|
165
167
|
for failure in critical:
|
|
166
|
-
reason = self.CRITICAL_HOOKS.get(
|
|
168
|
+
reason = self.CRITICAL_HOOKS.get(
|
|
169
|
+
failure.hook_name.lower(), "Security-critical check"
|
|
170
|
+
)
|
|
167
171
|
warnings.append(f" • {failure.hook_name}: {reason}")
|
|
168
|
-
|
|
172
|
+
|
|
169
173
|
if high:
|
|
170
174
|
warnings.append(
|
|
171
175
|
f"⚠️ HIGH: {len(high)} high-security checks failed - review recommended"
|
|
172
176
|
)
|
|
173
|
-
|
|
177
|
+
|
|
174
178
|
if medium:
|
|
175
|
-
warnings.append(
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
+
warnings.append(f"ℹ️ MEDIUM: {len(medium)} standard quality checks failed")
|
|
180
|
+
|
|
179
181
|
return warnings
|
|
180
|
-
|
|
182
|
+
|
|
181
183
|
def _generate_security_recommendations(
|
|
182
184
|
self,
|
|
183
185
|
critical: list[SecurityCheckResult],
|
|
184
186
|
high: list[SecurityCheckResult],
|
|
185
|
-
medium: list[SecurityCheckResult]
|
|
187
|
+
medium: list[SecurityCheckResult],
|
|
186
188
|
) -> list[str]:
|
|
187
189
|
"""Generate security recommendations based on OWASP best practices."""
|
|
188
190
|
recommendations = []
|
|
189
|
-
|
|
191
|
+
|
|
190
192
|
if critical:
|
|
191
|
-
recommendations.append(
|
|
192
|
-
|
|
193
|
+
recommendations.append(
|
|
194
|
+
"🔧 Fix all CRITICAL security issues before publishing"
|
|
195
|
+
)
|
|
196
|
+
|
|
193
197
|
# Specific recommendations based on failed checks
|
|
194
198
|
critical_names = [f.hook_name.lower() for f in critical]
|
|
195
|
-
|
|
196
|
-
if
|
|
197
|
-
recommendations.append(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if
|
|
201
|
-
recommendations.append(
|
|
202
|
-
|
|
199
|
+
|
|
200
|
+
if "bandit" in critical_names:
|
|
201
|
+
recommendations.append(
|
|
202
|
+
" • Review bandit security findings - may indicate vulnerabilities"
|
|
203
|
+
)
|
|
204
|
+
if "pyright" in critical_names:
|
|
205
|
+
recommendations.append(
|
|
206
|
+
" • Fix type errors - type safety prevents runtime security holes"
|
|
207
|
+
)
|
|
208
|
+
if "gitleaks" in critical_names:
|
|
209
|
+
recommendations.append(
|
|
210
|
+
" • Remove secrets/credentials from code - use environment variables"
|
|
211
|
+
)
|
|
212
|
+
|
|
203
213
|
if high:
|
|
204
|
-
recommendations.append(
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
recommendations.append(
|
|
215
|
+
"🔍 Review HIGH-security findings before production deployment"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if not critical and not high:
|
|
207
219
|
recommendations.append("✅ Security posture is acceptable for publishing")
|
|
208
|
-
|
|
220
|
+
|
|
209
221
|
# Add OWASP best practices reference
|
|
210
|
-
recommendations.append(
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
recommendations.append(
|
|
223
|
+
"📖 Follow OWASP Secure Coding Practices for comprehensive security"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return recommendations
|
crackerjack/services/config.py
CHANGED
|
@@ -53,8 +53,9 @@ class ConfigurationService:
|
|
|
53
53
|
)
|
|
54
54
|
return False
|
|
55
55
|
|
|
56
|
-
def get_temp_config_path(self) ->
|
|
57
|
-
|
|
56
|
+
def get_temp_config_path(self) -> str | None:
|
|
57
|
+
path = getattr(self, "_temp_config_path", None)
|
|
58
|
+
return str(path) if path else None
|
|
58
59
|
|
|
59
60
|
def _determine_config_mode(self, options: OptionsProtocol) -> str:
|
|
60
61
|
if options.experimental_hooks:
|
|
@@ -8,9 +8,11 @@ import tomli_w
|
|
|
8
8
|
import yaml
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
|
|
11
|
-
from crackerjack.models.protocols import
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
from crackerjack.models.protocols import (
|
|
12
|
+
ConfigMergeServiceProtocol,
|
|
13
|
+
FileSystemInterface,
|
|
14
|
+
GitInterface,
|
|
15
|
+
)
|
|
14
16
|
from crackerjack.services.logging import get_logger
|
|
15
17
|
|
|
16
18
|
|
|
@@ -29,8 +31,8 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
|
|
|
29
31
|
def __init__(
|
|
30
32
|
self,
|
|
31
33
|
console: Console,
|
|
32
|
-
filesystem:
|
|
33
|
-
git_service:
|
|
34
|
+
filesystem: FileSystemInterface,
|
|
35
|
+
git_service: GitInterface,
|
|
34
36
|
) -> None:
|
|
35
37
|
self.console = console
|
|
36
38
|
self.filesystem = filesystem
|
|
@@ -323,6 +325,8 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
|
|
|
323
325
|
content = buffer.getvalue().decode("utf-8")
|
|
324
326
|
|
|
325
327
|
# Clean trailing whitespace
|
|
328
|
+
from crackerjack.services.filesystem import FileSystemService
|
|
329
|
+
|
|
326
330
|
content = FileSystemService.clean_trailing_whitespace_and_newlines(content)
|
|
327
331
|
|
|
328
332
|
with target_path.open("w", encoding="utf-8") as f:
|
|
@@ -350,6 +354,8 @@ class ConfigMergeService(ConfigMergeServiceProtocol):
|
|
|
350
354
|
content = content or ""
|
|
351
355
|
|
|
352
356
|
# Clean trailing whitespace
|
|
357
|
+
from crackerjack.services.filesystem import FileSystemService
|
|
358
|
+
|
|
353
359
|
content = FileSystemService.clean_trailing_whitespace_and_newlines(content)
|
|
354
360
|
|
|
355
361
|
with target_path.open("w") as f:
|
|
@@ -52,6 +52,28 @@ class CoverageRatchetService:
|
|
|
52
52
|
def get_baseline(self) -> float:
|
|
53
53
|
return self.get_ratchet_data().get("baseline", 0.0)
|
|
54
54
|
|
|
55
|
+
def get_baseline_coverage(self) -> float:
|
|
56
|
+
"""Protocol method: Get baseline coverage."""
|
|
57
|
+
return self.get_baseline()
|
|
58
|
+
|
|
59
|
+
def update_baseline_coverage(self, new_coverage: float) -> bool:
|
|
60
|
+
"""Protocol method: Update baseline coverage."""
|
|
61
|
+
return self.update_coverage(new_coverage).get("success", False)
|
|
62
|
+
|
|
63
|
+
def is_coverage_regression(self, current_coverage: float) -> bool:
|
|
64
|
+
"""Protocol method: Check if coverage is a regression."""
|
|
65
|
+
baseline = self.get_baseline()
|
|
66
|
+
return current_coverage < (baseline - self.TOLERANCE_MARGIN)
|
|
67
|
+
|
|
68
|
+
def get_coverage_improvement_needed(self) -> float:
|
|
69
|
+
"""Protocol method: Get coverage improvement needed."""
|
|
70
|
+
data = self.get_ratchet_data()
|
|
71
|
+
baseline = data.get("baseline", 0.0)
|
|
72
|
+
next_milestone = data.get("next_milestone")
|
|
73
|
+
if next_milestone:
|
|
74
|
+
return next_milestone - baseline
|
|
75
|
+
return 100.0 - baseline
|
|
76
|
+
|
|
55
77
|
def update_coverage(self, new_coverage: float) -> dict[str, t.Any]:
|
|
56
78
|
"""Update coverage with 2% tolerance margin to prevent test flakiness.
|
|
57
79
|
|
crackerjack/services/git.py
CHANGED
|
@@ -6,6 +6,22 @@ from rich.console import Console
|
|
|
6
6
|
from .secure_subprocess import execute_secure_subprocess
|
|
7
7
|
from .security_logger import get_security_logger
|
|
8
8
|
|
|
9
|
+
# Centralized Git command registry for security validation
|
|
10
|
+
GIT_COMMANDS = {
|
|
11
|
+
"git_dir": ["rev-parse", "--git-dir"],
|
|
12
|
+
"staged_files": ["diff", "--cached", "--name-only", "--diff-filter=ACMRT"],
|
|
13
|
+
"unstaged_files": ["diff", "--name-only", "--diff-filter=ACMRT"],
|
|
14
|
+
"untracked_files": ["ls-files", "--others", "--exclude-standard"],
|
|
15
|
+
"staged_files_simple": ["diff", "--cached", "--name-only"],
|
|
16
|
+
"add_file": ["add"], # File path will be appended
|
|
17
|
+
"add_all": ["add", "-A", "."],
|
|
18
|
+
"commit": ["commit", "-m"], # Message will be appended
|
|
19
|
+
"add_updated": ["add", "-u"],
|
|
20
|
+
"push_porcelain": ["push", "--porcelain"],
|
|
21
|
+
"current_branch": ["branch", "--show-current"],
|
|
22
|
+
"commits_ahead": ["rev-list", "--count", "@{u}..HEAD"],
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
|
|
10
26
|
class FailedGitResult:
|
|
11
27
|
"""A Git result object compatible with subprocess.CompletedProcess."""
|
|
@@ -51,34 +67,28 @@ class GitService:
|
|
|
51
67
|
|
|
52
68
|
def is_git_repo(self) -> bool:
|
|
53
69
|
try:
|
|
54
|
-
result = self._run_git_command(["
|
|
70
|
+
result = self._run_git_command(GIT_COMMANDS["git_dir"])
|
|
55
71
|
return result.returncode == 0
|
|
56
72
|
except (subprocess.SubprocessError, OSError, FileNotFoundError):
|
|
57
73
|
return False
|
|
58
74
|
|
|
59
75
|
def get_changed_files(self) -> list[str]:
|
|
60
76
|
try:
|
|
61
|
-
staged_result = self._run_git_command(
|
|
62
|
-
["diff", "--cached", "- - name-only", "- - diff-filter=ACMRT"]
|
|
63
|
-
)
|
|
77
|
+
staged_result = self._run_git_command(GIT_COMMANDS["staged_files"])
|
|
64
78
|
staged_files = (
|
|
65
79
|
staged_result.stdout.strip().split("\n")
|
|
66
80
|
if staged_result.stdout.strip()
|
|
67
81
|
else []
|
|
68
82
|
)
|
|
69
83
|
|
|
70
|
-
unstaged_result = self._run_git_command(
|
|
71
|
-
["diff", "- - name-only", "- - diff-filter=ACMRT"]
|
|
72
|
-
)
|
|
84
|
+
unstaged_result = self._run_git_command(GIT_COMMANDS["unstaged_files"])
|
|
73
85
|
unstaged_files = (
|
|
74
86
|
unstaged_result.stdout.strip().split("\n")
|
|
75
87
|
if unstaged_result.stdout.strip()
|
|
76
88
|
else []
|
|
77
89
|
)
|
|
78
90
|
|
|
79
|
-
untracked_result = self._run_git_command(
|
|
80
|
-
["ls-files", "--others", "- - exclude-standard"],
|
|
81
|
-
)
|
|
91
|
+
untracked_result = self._run_git_command(GIT_COMMANDS["untracked_files"])
|
|
82
92
|
untracked_files = (
|
|
83
93
|
untracked_result.stdout.strip().split("\n")
|
|
84
94
|
if untracked_result.stdout.strip()
|
|
@@ -93,7 +103,7 @@ class GitService:
|
|
|
93
103
|
|
|
94
104
|
def get_staged_files(self) -> list[str]:
|
|
95
105
|
try:
|
|
96
|
-
result = self._run_git_command(["
|
|
106
|
+
result = self._run_git_command(GIT_COMMANDS["staged_files_simple"])
|
|
97
107
|
return result.stdout.strip().split("\n") if result.stdout.strip() else []
|
|
98
108
|
except Exception as e:
|
|
99
109
|
self.console.print(f"[yellow]⚠️[/ yellow] Error getting staged files: {e}")
|
|
@@ -102,7 +112,8 @@ class GitService:
|
|
|
102
112
|
def add_files(self, files: list[str]) -> bool:
|
|
103
113
|
try:
|
|
104
114
|
for file in files:
|
|
105
|
-
|
|
115
|
+
cmd = GIT_COMMANDS["add_file"] + [file]
|
|
116
|
+
result = self._run_git_command(cmd)
|
|
106
117
|
if result.returncode != 0:
|
|
107
118
|
self.console.print(
|
|
108
119
|
f"[red]❌[/ red] Failed to add {file}: {result.stderr}",
|
|
@@ -116,7 +127,7 @@ class GitService:
|
|
|
116
127
|
def add_all_files(self) -> bool:
|
|
117
128
|
"""Stage all changes including new, modified, and deleted files."""
|
|
118
129
|
try:
|
|
119
|
-
result = self._run_git_command(["
|
|
130
|
+
result = self._run_git_command(GIT_COMMANDS["add_all"])
|
|
120
131
|
if result.returncode == 0:
|
|
121
132
|
self.console.print("[green]✅[/ green] Staged all changes")
|
|
122
133
|
return True
|
|
@@ -130,7 +141,8 @@ class GitService:
|
|
|
130
141
|
|
|
131
142
|
def commit(self, message: str) -> bool:
|
|
132
143
|
try:
|
|
133
|
-
|
|
144
|
+
cmd = GIT_COMMANDS["commit"] + [message]
|
|
145
|
+
result = self._run_git_command(cmd)
|
|
134
146
|
if result.returncode == 0:
|
|
135
147
|
self.console.print(f"[green]✅[/ green] Committed: {message}")
|
|
136
148
|
return True
|
|
@@ -153,14 +165,15 @@ class GitService:
|
|
|
153
165
|
"[yellow]🔄[/ yellow] Pre - commit hooks modified files - attempting to re-stage and retry commit"
|
|
154
166
|
)
|
|
155
167
|
|
|
156
|
-
add_result = self._run_git_command(["
|
|
168
|
+
add_result = self._run_git_command(GIT_COMMANDS["add_updated"])
|
|
157
169
|
if add_result.returncode != 0:
|
|
158
170
|
self.console.print(
|
|
159
171
|
f"[red]❌[/ red] Failed to re-stage files: {add_result.stderr}"
|
|
160
172
|
)
|
|
161
173
|
return False
|
|
162
174
|
|
|
163
|
-
|
|
175
|
+
cmd = GIT_COMMANDS["commit"] + [message]
|
|
176
|
+
retry_result = self._run_git_command(cmd)
|
|
164
177
|
if retry_result.returncode == 0:
|
|
165
178
|
self.console.print(
|
|
166
179
|
f"[green]✅[/ green] Committed after re-staging: {message}"
|
|
@@ -188,7 +201,7 @@ class GitService:
|
|
|
188
201
|
def push(self) -> bool:
|
|
189
202
|
try:
|
|
190
203
|
# Get detailed push information
|
|
191
|
-
result = self._run_git_command(["
|
|
204
|
+
result = self._run_git_command(GIT_COMMANDS["push_porcelain"])
|
|
192
205
|
if result.returncode == 0:
|
|
193
206
|
self._display_push_success(result.stdout)
|
|
194
207
|
return True
|
|
@@ -241,7 +254,7 @@ class GitService:
|
|
|
241
254
|
"""Fallback method to show commit count information."""
|
|
242
255
|
try:
|
|
243
256
|
# Get commits ahead of remote
|
|
244
|
-
result = self._run_git_command(["
|
|
257
|
+
result = self._run_git_command(GIT_COMMANDS["commits_ahead"])
|
|
245
258
|
if result.returncode == 0 and result.stdout.strip().isdigit():
|
|
246
259
|
commit_count = int(result.stdout.strip())
|
|
247
260
|
if commit_count > 0:
|
|
@@ -260,17 +273,17 @@ class GitService:
|
|
|
260
273
|
|
|
261
274
|
def get_current_branch(self) -> str | None:
|
|
262
275
|
try:
|
|
263
|
-
result = self._run_git_command(["
|
|
276
|
+
result = self._run_git_command(GIT_COMMANDS["current_branch"])
|
|
264
277
|
return result.stdout.strip() if result.returncode == 0 else None
|
|
265
278
|
except (subprocess.SubprocessError, OSError, FileNotFoundError):
|
|
266
279
|
return None
|
|
267
280
|
|
|
268
|
-
def get_commit_message_suggestions(self,
|
|
269
|
-
if not
|
|
281
|
+
def get_commit_message_suggestions(self, changed_files: list[str]) -> list[str]:
|
|
282
|
+
if not changed_files:
|
|
270
283
|
return ["Update project files"]
|
|
271
|
-
file_categories = self._categorize_files(
|
|
284
|
+
file_categories = self._categorize_files(changed_files)
|
|
272
285
|
messages = self._generate_category_messages(file_categories)
|
|
273
|
-
messages.extend(self._generate_specific_messages(
|
|
286
|
+
messages.extend(self._generate_specific_messages(changed_files))
|
|
274
287
|
|
|
275
288
|
return messages[:5]
|
|
276
289
|
|
|
@@ -326,7 +339,7 @@ class GitService:
|
|
|
326
339
|
from contextlib import suppress
|
|
327
340
|
|
|
328
341
|
with suppress(ValueError, Exception):
|
|
329
|
-
result = self._run_git_command(["
|
|
342
|
+
result = self._run_git_command(GIT_COMMANDS["commits_ahead"])
|
|
330
343
|
if result.returncode == 0 and result.stdout.strip().isdigit():
|
|
331
344
|
return int(result.stdout.strip())
|
|
332
345
|
return 0
|