deployproof 0.1.8__tar.gz → 0.2.0__tar.gz

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.
Files changed (41) hide show
  1. {deployproof-0.1.8/src/deployproof.egg-info → deployproof-0.2.0}/PKG-INFO +6 -6
  2. {deployproof-0.1.8 → deployproof-0.2.0}/README.md +5 -5
  3. {deployproof-0.1.8 → deployproof-0.2.0}/pyproject.toml +1 -1
  4. deployproof-0.2.0/src/deployproof/__init__.py +2 -0
  5. deployproof-0.2.0/src/deployproof/cli.py +179 -0
  6. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/dependencies.py +10 -5
  7. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/mutator.py +196 -201
  8. deployproof-0.2.0/src/deployproof/reporter.py +333 -0
  9. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/symlinks.py +22 -5
  10. {deployproof-0.1.8 → deployproof-0.2.0/src/deployproof.egg-info}/PKG-INFO +6 -6
  11. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_cli.py +3 -3
  12. deployproof-0.2.0/tests/test_mutator.py +375 -0
  13. deployproof-0.1.8/src/deployproof/__init__.py +0 -2
  14. deployproof-0.1.8/src/deployproof/cli.py +0 -400
  15. deployproof-0.1.8/src/deployproof/reporter.py +0 -636
  16. deployproof-0.1.8/tests/test_mutator.py +0 -154
  17. {deployproof-0.1.8 → deployproof-0.2.0}/LICENSE +0 -0
  18. {deployproof-0.1.8 → deployproof-0.2.0}/setup.cfg +0 -0
  19. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/control_flow.py +0 -0
  20. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/diff.py +0 -0
  21. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/mocks.py +0 -0
  22. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/secrets.py +0 -0
  23. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof/wsl.py +0 -0
  24. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof.egg-info/SOURCES.txt +0 -0
  25. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof.egg-info/dependency_links.txt +0 -0
  26. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof.egg-info/entry_points.txt +0 -0
  27. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof.egg-info/requires.txt +0 -0
  28. {deployproof-0.1.8 → deployproof-0.2.0}/src/deployproof.egg-info/top_level.txt +0 -0
  29. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_baseline_collection_error.py +0 -0
  30. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_cli_dependency_scan.py +0 -0
  31. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_control_flow.py +0 -0
  32. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_dependencies_extractor.py +0 -0
  33. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_dependencies_pypi.py +0 -0
  34. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_diff.py +0 -0
  35. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_edge_cases_validation.py +0 -0
  36. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_mocks.py +0 -0
  37. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_mutation_suites.py +0 -0
  38. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_secrets.py +0 -0
  39. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_stress_regressions.py +0 -0
  40. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_symlinks.py +0 -0
  41. {deployproof-0.1.8 → deployproof-0.2.0}/tests/test_wsl.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deployproof
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: A deterministic AI-code deployability checker that catches what coverage misses.
5
5
  Author-email: SVS Praveen <svspraveens@gmail.com>
6
6
  License-Expression: MIT
@@ -32,10 +32,10 @@ Dynamic: license-file
32
32
 
33
33
  > Deterministic pre-push verification for AI-assisted codebases: AST mutation testing, credential scanning, sandbox-escape detection, mock-usage alerts, swallowed-exception checks, and dependency hallucination defense.
34
34
 
35
- [![PyPI version](https://img.shields.io/badge/pypi-v0.1.8-007ec6.svg)](https://pypi.org/project/deployproof/)
35
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.2.0-007ec6.svg)](https://pypi.org/project/deployproof/)
36
36
  [![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-3776ab.svg)](https://pypi.org/project/deployproof/)
37
37
  [![CI](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml/badge.svg)](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
38
- [![Tests](https://img.shields.io/badge/tests-88%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
38
+ [![Tests](https://img.shields.io/badge/tests-93%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
39
39
  [![Stress Tests](https://img.shields.io/badge/stress%20tests-11%2F11%20passed-2ea44f.svg)](stress_fixtures/)
40
40
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
41
41
 
@@ -189,7 +189,7 @@ deployproof check --json
189
189
 
190
190
  ```json
191
191
  {
192
- "version": "0.1.8",
192
+ "version": "0.2.0",
193
193
  "status": "passed",
194
194
  "summary": {
195
195
  "target_files_count": 1,
@@ -262,7 +262,7 @@ deployproof check --json
262
262
 
263
263
  ## What It Checks
264
264
 
265
- - **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage.
265
+ - **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage. Features atomic file restoration protected by `SIGINT`/`SIGTERM`/`SIGBREAK` signal handlers to ensure interrupted runs never leave mutated code on disk.
266
266
  - **Secrets and Credentials** — Scans modified files for hardcoded API keys (OpenAI, Anthropic, AWS, GitHub, Stripe, private keys) and tracked `.env` files using pattern matching and entropy analysis.
267
267
  - **Symlink and Sandbox Escape** — Resolves symbolic links and flags any whose target escapes the repository root (CWE-61 / CWE-451). Catches the class of path-traversal trick used in the GhostApproval disclosure (Wiz Research, July 2026).
268
268
  - **Dependency and Slopsquatting** — For each new import, dynamic import (`importlib.import_module`, `__import__`), or manifest entry (including recursive `-r` includes) introduced in the diff, queries the PyPI JSON API and checks registration age. Packages that don't exist (HTTP 404) are flagged HIGH RISK; packages registered within the last 30 days are flagged MEDIUM RISK.
@@ -288,7 +288,7 @@ Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS cr
288
288
 
289
289
  ## Status & Roadmap
290
290
 
291
- - **Current (v0.1.8):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery and precise column-offset snippet reconstruction), baseline test-collection failure isolation with distinct exit code `2`, entropy-driven value-based secrets scanner (including unquoted .env values and prefix validation), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with import-to-distribution translation, recursive `-r` requirements scanning, and dynamic import detection via `importlib` / `__import__`), mock-introduction detector (`--strict-mocks` and deduplicated decorator detection), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 88 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
291
+ - **Current (v0.2.0):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery, AST column-offset snippet reconstruction, and `SIGINT`/`SIGTERM`/`SIGBREAK` signal-safe disk restoration), baseline test-collection failure isolation with distinct exit code `2`, entropy-driven value-based secrets scanner (including unquoted .env values and prefix validation), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with import-to-distribution translation, recursive `-r` requirements scanning, and dynamic import detection via `importlib` / `__import__`), mock-introduction detector (`--strict-mocks`), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 93 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
292
292
  - **Next:** Multi-language mutation support and expanded ecosystem rule packs.
293
293
 
294
294
  ## Contributing
@@ -2,10 +2,10 @@
2
2
 
3
3
  > Deterministic pre-push verification for AI-assisted codebases: AST mutation testing, credential scanning, sandbox-escape detection, mock-usage alerts, swallowed-exception checks, and dependency hallucination defense.
4
4
 
5
- [![PyPI version](https://img.shields.io/badge/pypi-v0.1.8-007ec6.svg)](https://pypi.org/project/deployproof/)
5
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.2.0-007ec6.svg)](https://pypi.org/project/deployproof/)
6
6
  [![Python versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-3776ab.svg)](https://pypi.org/project/deployproof/)
7
7
  [![CI](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml/badge.svg)](https://github.com/SVSPraveen/DeployProof/actions/workflows/ci.yml)
8
- [![Tests](https://img.shields.io/badge/tests-88%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
8
+ [![Tests](https://img.shields.io/badge/tests-93%20passed-2ea44f.svg)](https://github.com/SVSPraveen/DeployProof)
9
9
  [![Stress Tests](https://img.shields.io/badge/stress%20tests-11%2F11%20passed-2ea44f.svg)](stress_fixtures/)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
11
11
 
@@ -159,7 +159,7 @@ deployproof check --json
159
159
 
160
160
  ```json
161
161
  {
162
- "version": "0.1.8",
162
+ "version": "0.2.0",
163
163
  "status": "passed",
164
164
  "summary": {
165
165
  "target_files_count": 1,
@@ -232,7 +232,7 @@ deployproof check --json
232
232
 
233
233
  ## What It Checks
234
234
 
235
- - **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage.
235
+ - **Mutation Score** — Mutates AST operators (`>=`, `==`, `and`, `or`, `*`, numeric constants, comparisons) in modified files and runs your test suite against each mutant. Reports surviving mutants and a percentage score. Does not use line coverage. Features atomic file restoration protected by `SIGINT`/`SIGTERM`/`SIGBREAK` signal handlers to ensure interrupted runs never leave mutated code on disk.
236
236
  - **Secrets and Credentials** — Scans modified files for hardcoded API keys (OpenAI, Anthropic, AWS, GitHub, Stripe, private keys) and tracked `.env` files using pattern matching and entropy analysis.
237
237
  - **Symlink and Sandbox Escape** — Resolves symbolic links and flags any whose target escapes the repository root (CWE-61 / CWE-451). Catches the class of path-traversal trick used in the GhostApproval disclosure (Wiz Research, July 2026).
238
238
  - **Dependency and Slopsquatting** — For each new import, dynamic import (`importlib.import_module`, `__import__`), or manifest entry (including recursive `-r` includes) introduced in the diff, queries the PyPI JSON API and checks registration age. Packages that don't exist (HTTP 404) are flagged HIGH RISK; packages registered within the last 30 days are flagged MEDIUM RISK.
@@ -258,7 +258,7 @@ Fixtures cover weak test suites, zero-test orphan modules, planted OpenAI/AWS cr
258
258
 
259
259
  ## Status & Roadmap
260
260
 
261
- - **Current (v0.1.8):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery and precise column-offset snippet reconstruction), baseline test-collection failure isolation with distinct exit code `2`, entropy-driven value-based secrets scanner (including unquoted .env values and prefix validation), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with import-to-distribution translation, recursive `-r` requirements scanning, and dynamic import detection via `importlib` / `__import__`), mock-introduction detector (`--strict-mocks` and deduplicated decorator detection), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 88 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
261
+ - **Current (v0.2.0):** Diff-scoped AST mutation testing (with recursive `test/`/`tests/` discovery, AST column-offset snippet reconstruction, and `SIGINT`/`SIGTERM`/`SIGBREAK` signal-safe disk restoration), baseline test-collection failure isolation with distinct exit code `2`, entropy-driven value-based secrets scanner (including unquoted .env values and prefix validation), GhostApproval symlink sandbox-escape detector, PyPI dependency hallucination / slopsquatting scanner (with import-to-distribution translation, recursive `-r` requirements scanning, and dynamic import detection via `importlib` / `__import__`), mock-introduction detector (`--strict-mocks`), control-flow / swallowed-exception scanner (`--strict-error-handling`), 11/11 launch-day stress test suite, 93 unit tests, machine-readable `--json` output, and Tier 2 CI verification via GitHub Actions (mutmut).
262
262
  - **Next:** Multi-language mutation support and expanded ecosystem rule packs.
263
263
 
264
264
  ## Contributing
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deployproof"
7
- version = "0.1.8"
7
+ version = "0.2.0"
8
8
  description = "A deterministic AI-code deployability checker that catches what coverage misses."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,2 @@
1
+ """DeployProof - Deterministic AI-Code Deployability Checker."""
2
+ __version__ = "0.2.0"
@@ -0,0 +1,179 @@
1
+ """Command-line interface for DeployProof."""
2
+ import argparse
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+ from deployproof import __version__
7
+ from deployproof.dependencies import extract_all_new_dependencies, scan_dependencies
8
+ from deployproof.control_flow import scan_session_files_for_control_flow
9
+ from deployproof.diff import DiffScopeError, InvalidBaseRefError, NotAGitRepositoryError, get_git_root, is_test_file, resolve_changed_python_files, resolve_changed_session_files
10
+ from deployproof.mocks import scan_session_files_for_mocks
11
+ from deployproof.mutator import run_mutation_tests
12
+ from deployproof.reporter import LARGE_FILE_LOC_THRESHOLD, format_json_report, format_report
13
+ from deployproof.secrets import scan_session_files_for_secrets
14
+ from deployproof.symlinks import scan_session_files_for_symlinks
15
+ from deployproof.wsl import check_wsl_readiness, run_wsl_mutmut
16
+
17
+ def create_parser() -> argparse.ArgumentParser:
18
+ """Create and configure the command-line argument parser."""
19
+ parser = argparse.ArgumentParser(prog='deployproof', description='DeployProof: A deterministic AI-code deployability checker.')
20
+ parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version__}', help="Show program's version number and exit.")
21
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
22
+ check_parser = subparsers.add_parser('check', help='Run deployability verification checks against session changes.')
23
+ check_parser.add_argument('--base', type=str, default=None, help='Base git ref (branch/commit/tag) to diff against.')
24
+ check_parser.add_argument('--files', nargs='+', type=str, default=None, help='Explicit files to evaluate (bypasses git diff).')
25
+ check_parser.add_argument('--tests', nargs='+', type=str, default=None, help='Specific test file(s) or directories to execute.')
26
+ check_parser.add_argument('--threshold', type=float, default=80.0, help='Minimum mutation score percentage to pass (default: 80.0).')
27
+ check_parser.add_argument('--timeout', type=float, default=10.0, help='Test runner timeout in seconds per mutant (default: 10.0).')
28
+ check_parser.add_argument('--wsl', action='store_true', default=False, help='Delegate mutation testing to mutmut inside WSL (Windows only).')
29
+ check_parser.add_argument('--strict-mocks', action='store_true', default=False, help='Fail the verification gate if new mock or monkeypatch usage is introduced in test files.')
30
+ check_parser.add_argument('--strict-error-handling', action='store_true', default=False, help='Fail the verification gate if bare excepts, swallowed exceptions, or unreachable code are detected.')
31
+ check_parser.add_argument('--json', action='store_true', default=False, help='Output check results as a structured JSON object.')
32
+ subparsers.add_parser('init', help='Initialize DeployProof configuration in the current repository.')
33
+ return parser
34
+
35
+ def handle_check(args: argparse.Namespace) -> int:
36
+ """Handle the 'check' subcommand."""
37
+ cwd = Path.cwd().resolve()
38
+ repo_root: Optional[Path] = None
39
+ try:
40
+ if args.files:
41
+ session_files = [Path(f).resolve() for f in args.files]
42
+ if session_files:
43
+ try:
44
+ repo_root = get_git_root(session_files[0].parent)
45
+ except DiffScopeError:
46
+ repo_root = session_files[0].parent
47
+ else:
48
+ repo_root = cwd
49
+ else:
50
+ repo_root = get_git_root(cwd)
51
+ session_files = resolve_changed_session_files(cwd=cwd, base=args.base)
52
+ except NotAGitRepositoryError:
53
+ print('Error: Not a git repository. Initialize git or specify files with --files.', file=sys.stderr)
54
+ return 1
55
+ except InvalidBaseRefError as e:
56
+ print(f'Error: {e}', file=sys.stderr)
57
+ return 1
58
+ except DiffScopeError as e:
59
+ print(f'Error: {e}', file=sys.stderr)
60
+ return 1
61
+ if not session_files:
62
+ if getattr(args, 'json', False):
63
+ import json
64
+ print(json.dumps({'version': __version__, 'status': 'passed', 'message': 'No modified files detected in current session.', 'scope': {'target_files': []}, 'summary': {'target_files_count': 0, 'mutation_score': 100.0, 'threshold': args.threshold, 'secrets_found': 0, 'symlink_escapes_found': 0, 'dependency_findings': {'high_risk': 0, 'medium_risk': 0, 'ok': 0, 'unknown': 0, 'unscanned': 0}, 'mock_usages_found': 0, 'control_flow_findings': 0, 'strict_mocks_active': getattr(args, 'strict_mocks', False), 'strict_mocks_triggered': False, 'strict_error_handling_active': getattr(args, 'strict_error_handling', False), 'strict_error_handling_triggered': False}, 'control_flow': {'clean': True, 'strict_gate_triggered': False, 'findings': []}}, indent=2))
65
+ return 0
66
+ else:
67
+ print('DeployProof: No modified files detected in current session.')
68
+ print('Working tree is clean. Use --base <ref> or --files <path...> to evaluate specific files.')
69
+ return 0
70
+ symlink_result = scan_session_files_for_symlinks(session_files, repo_root=repo_root or cwd)
71
+ secrets_result = scan_session_files_for_secrets(session_files)
72
+ extracted_deps = extract_all_new_dependencies(session_files, root=repo_root or cwd, base=args.base)
73
+ dependency_result = scan_dependencies(extracted_deps)
74
+ mock_result = scan_session_files_for_mocks(session_files=session_files, root=repo_root or cwd, base=args.base)
75
+ control_flow_result = scan_session_files_for_control_flow(session_files=session_files, root=repo_root or cwd, base=args.base)
76
+ if args.files:
77
+ non_test_files = [f for f in session_files if f.is_file() and f.suffix == '.py' and (not is_test_file(f))]
78
+ target_files = non_test_files if non_test_files else [f for f in session_files if f.is_file() and f.suffix == '.py']
79
+ else:
80
+ target_files = [f for f in session_files if f.is_file() and f.suffix == '.py' and (not is_test_file(f))]
81
+ if not getattr(args, 'json', False):
82
+ for f in target_files:
83
+ try:
84
+ loc = len(f.read_text(encoding='utf-8', errors='replace').splitlines())
85
+ if loc < LARGE_FILE_LOC_THRESHOLD:
86
+ try:
87
+ rel = f.relative_to(repo_root or cwd)
88
+ except ValueError:
89
+ rel = f
90
+ print(f"Notice: Large file '{rel}' ({loc} LOC) detected - mutation testing may take several minutes.")
91
+ except Exception:
92
+ pass
93
+ if getattr(args, 'wsl', False):
94
+ wsl_ready, wsl_msg = check_wsl_readiness()
95
+ if wsl_ready:
96
+ if not getattr(args, 'json', False):
97
+ print('DeployProof - Delegating to mutmut in WSL...')
98
+ wsl_res = run_wsl_mutmut(repo_root or cwd, target_files)
99
+ if wsl_res.get('success'):
100
+ if not getattr(args, 'json', False):
101
+ print(wsl_res.get('stdout', ''))
102
+ has_security_issue = bool(secrets_result.findings or symlink_result.escape_findings or dependency_result.high_risk_count > 0 or (getattr(args, 'strict_mocks', False) and mock_result.total_findings > 0) or (getattr(args, 'strict_error_handling', False) and control_flow_result.total_findings > 0))
103
+ return 1 if has_security_issue else 0
104
+ elif not getattr(args, 'json', False):
105
+ print(f"WSL execution error: {wsl_res.get('error') or wsl_res.get('stderr')}", file=sys.stderr)
106
+ print('Falling back to Tier 1 local pre-check...')
107
+ elif not getattr(args, 'json', False):
108
+ print(wsl_msg)
109
+ print('-' * 68)
110
+ result = run_mutation_tests(target_files=target_files, repo_root=repo_root or cwd, test_runner_timeout=args.timeout, extra_pytest_args=args.tests)
111
+ if getattr(args, 'json', False):
112
+ report_text = format_json_report(result=result, target_files=target_files, secrets_result=secrets_result, symlink_result=symlink_result, dependency_result=dependency_result, mock_result=mock_result, control_flow_result=control_flow_result, strict_mocks=getattr(args, 'strict_mocks', False), strict_error_handling=getattr(args, 'strict_error_handling', False), repo_root=repo_root or cwd, threshold=args.threshold, version=__version__)
113
+ else:
114
+ report_text = format_report(result=result, target_files=target_files, secrets_result=secrets_result, symlink_result=symlink_result, dependency_result=dependency_result, mock_result=mock_result, control_flow_result=control_flow_result, strict_mocks=getattr(args, 'strict_mocks', False), strict_error_handling=getattr(args, 'strict_error_handling', False), repo_root=repo_root or cwd, threshold=args.threshold)
115
+ print(report_text)
116
+ if result.collection_error:
117
+ return 2
118
+ strict_mocks_triggered = bool(getattr(args, 'strict_mocks', False) and mock_result.total_findings > 0)
119
+ strict_error_triggered = bool(getattr(args, 'strict_error_handling', True) and control_flow_result.total_findings > 0)
120
+ if result.mutation_score is not None and result.mutation_score < args.threshold or len(result.untested_files) > 0 or len(secrets_result.findings) > 0 or (len(symlink_result.escape_findings) > 0) or (dependency_result.high_risk_count > 0) or strict_mocks_triggered or strict_error_triggered:
121
+ return 1
122
+ return 0
123
+
124
+ def handle_init(args: argparse.Namespace) -> int:
125
+ """Handle the 'init' subcommand to initialize configuration and git hooks."""
126
+ cwd = Path.cwd().resolve()
127
+ try:
128
+ repo_root = get_git_root(cwd)
129
+ except DiffScopeError:
130
+ repo_root = cwd
131
+ print(f'DeployProof: Initializing in {repo_root}...')
132
+ config_path = repo_root / '.deployproof.json'
133
+ if not config_path.exists():
134
+ import json
135
+ default_config = {'version': __version__, 'threshold': 80.0, 'timeout': 10.0, 'secrets_scanning': True, 'symlink_scanning': True, 'dependency_scanning': True}
136
+ config_path.write_text(json.dumps(default_config, indent=2) + '\n', encoding='utf-8')
137
+ print(f' [+] Created configuration file: {config_path.name}')
138
+ else:
139
+ print(f' [.] Configuration file already exists: {config_path.name}')
140
+ hooks_dir = repo_root / '.git' / 'hooks'
141
+ if hooks_dir.is_dir():
142
+ pre_push_hook = hooks_dir / 'pre-push'
143
+ hook_script = '#!/usr/bin/env sh\n# DeployProof deterministic pre-push verification gate\ndeployproof check\n'
144
+ pre_push_hook.write_text(hook_script, encoding='utf-8')
145
+ try:
146
+ import stat
147
+ pre_push_hook.chmod(pre_push_hook.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
148
+ except Exception:
149
+ pass
150
+ print(' [+] Installed git pre-push hook: .git/hooks/pre-push')
151
+ else:
152
+ print(' [i] Note: .git/hooks directory not found. Initialize git to enable automatic pre-push gating.')
153
+ print("\nDeployProof initialization complete. Run 'deployproof check' to verify your session changes.")
154
+ return 0
155
+
156
+ def main(argv: Optional[List[str]]=None) -> int:
157
+ """Entry point for the DeployProof CLI."""
158
+ if hasattr(sys.stdout, 'reconfigure'):
159
+ try:
160
+ sys.stdout.reconfigure(errors='replace')
161
+ except Exception:
162
+ pass
163
+ if hasattr(sys.stderr, 'reconfigure'):
164
+ try:
165
+ sys.stderr.reconfigure(errors='replace')
166
+ except Exception:
167
+ pass
168
+ parser = create_parser()
169
+ args = parser.parse_args(argv)
170
+ if not args.command:
171
+ parser.print_help()
172
+ return 0
173
+ if args.command == 'check':
174
+ return handle_check(args)
175
+ elif args.command == 'init':
176
+ return handle_init(args)
177
+ return 0
178
+ if __name__ == '__main__':
179
+ sys.exit(main())
@@ -983,7 +983,7 @@ def query_pypi_registry(
983
983
  url = f"https://pypi.org/pypi/{canonical_pkg}/json"
984
984
  req = urllib.request.Request(
985
985
  url,
986
- headers={"User-Agent": "DeployProof/0.1.8 (https://github.com/SVSPraveen/DeployProof)"},
986
+ headers={"User-Agent": "DeployProof/0.2.0 (https://github.com/SVSPraveen/DeployProof)"},
987
987
  )
988
988
 
989
989
  try:
@@ -993,6 +993,7 @@ def query_pypi_registry(
993
993
  data = json.loads(raw_bytes.decode("utf-8"))
994
994
  releases = data.get("releases", {})
995
995
  timestamps: List[datetime.datetime] = []
996
+ parse_error: Optional[str] = None
996
997
 
997
998
  for ver, files in releases.items():
998
999
  for f in files:
@@ -1001,10 +1002,14 @@ def query_pypi_registry(
1001
1002
  try:
1002
1003
  dt = datetime.datetime.fromisoformat(t_str.replace("Z", "+00:00"))
1003
1004
  timestamps.append(dt)
1004
- except Exception:
1005
- pass
1006
-
1007
- if not timestamps:
1005
+ except (ValueError, TypeError) as e:
1006
+ parse_error = f"Failed to parse release timestamp '{t_str}': {e}"
1007
+ except Exception as e:
1008
+ parse_error = f"Unexpected error parsing release timestamp '{t_str}': {e}"
1009
+
1010
+ if parse_error and not timestamps:
1011
+ result = ("UNKNOWN", None, None, f"PyPI release date parse error for package '{canonical_pkg}': {parse_error}")
1012
+ elif not timestamps:
1008
1013
  if canonical_pkg.lower() in KNOWN_NAMESPACE_ROOTS or norm_name.lower() in KNOWN_NAMESPACE_ROOTS:
1009
1014
  result = ("OK", None, None, f"Namespace package umbrella root ('{canonical_pkg}') - subpackages published separately on PyPI")
1010
1015
  else: