nextog-cli 1.0.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.
Files changed (51) hide show
  1. nextog/__init__.py +4 -0
  2. nextog/cli.py +545 -0
  3. nextog/config/__init__.py +1 -0
  4. nextog/config/settings.py +132 -0
  5. nextog/core/__init__.py +1 -0
  6. nextog/core/engine.py +193 -0
  7. nextog/core/permissions.py +129 -0
  8. nextog/core/privacy.py +130 -0
  9. nextog/core/reporter.py +204 -0
  10. nextog/core/runner.py +236 -0
  11. nextog/data/__init__.py +1 -0
  12. nextog/data/local_db.py +367 -0
  13. nextog/data/models.py +72 -0
  14. nextog/data/sync.py +65 -0
  15. nextog/engines/__init__.py +1 -0
  16. nextog/engines/api/__init__.py +1 -0
  17. nextog/engines/api/graphql.py +54 -0
  18. nextog/engines/api/rest.py +346 -0
  19. nextog/engines/api/websocket.py +59 -0
  20. nextog/engines/embedded/__init__.py +1 -0
  21. nextog/engines/embedded/firmware.py +53 -0
  22. nextog/engines/embedded/hardware.py +330 -0
  23. nextog/engines/mobile/__init__.py +1 -0
  24. nextog/engines/mobile/android.py +333 -0
  25. nextog/engines/mobile/cross.py +48 -0
  26. nextog/engines/mobile/ios.py +46 -0
  27. nextog/engines/system/__init__.py +1 -0
  28. nextog/engines/system/load.py +121 -0
  29. nextog/engines/system/performance.py +128 -0
  30. nextog/engines/system/security.py +170 -0
  31. nextog/engines/web/__init__.py +1 -0
  32. nextog/engines/web/accessibility.py +191 -0
  33. nextog/engines/web/browser.py +387 -0
  34. nextog/engines/web/elements.py +285 -0
  35. nextog/engines/web/responsive.py +79 -0
  36. nextog/live/__init__.py +1 -0
  37. nextog/live/dashboard.py +30 -0
  38. nextog/live/panel.py +325 -0
  39. nextog/reports/__init__.py +1359 -0
  40. nextog/training/__init__.py +1 -0
  41. nextog/training/learner.py +269 -0
  42. nextog/training/patterns.py +102 -0
  43. nextog/utils/__init__.py +1 -0
  44. nextog/utils/helpers.py +91 -0
  45. nextog/utils/logger.py +37 -0
  46. nextog/utils/validators.py +98 -0
  47. nextog_cli-1.0.0.dist-info/METADATA +344 -0
  48. nextog_cli-1.0.0.dist-info/RECORD +51 -0
  49. nextog_cli-1.0.0.dist-info/WHEEL +5 -0
  50. nextog_cli-1.0.0.dist-info/entry_points.txt +2 -0
  51. nextog_cli-1.0.0.dist-info/top_level.txt +1 -0
nextog/core/engine.py ADDED
@@ -0,0 +1,193 @@
1
+ """
2
+ Core Test Engine - Orchestrates all testing operations
3
+ """
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import Dict, List, Optional, Any
8
+ from pathlib import Path
9
+ from datetime import datetime
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
14
+
15
+ class TestEngine:
16
+ """Main testing engine that orchestrates all test types"""
17
+
18
+ def __init__(self, settings, db, privacy):
19
+ self.settings = settings
20
+ self.db = db
21
+ self.privacy = privacy
22
+ self.results: Dict[str, Any] = {}
23
+ self.coverage = 0.0
24
+ self.start_time = None
25
+
26
+ def run_all_tests(self, config: Dict, coverage_target: int = 90) -> Dict:
27
+ """Run all configured tests with coverage tracking"""
28
+ self.start_time = time.time()
29
+ self.results = {
30
+ "categories": {},
31
+ "total_coverage": 0,
32
+ "timestamp": datetime.now().isoformat(),
33
+ "duration": 0,
34
+ }
35
+
36
+ # Phase 1: Smoke Tests (0% - 20%)
37
+ self._run_phase("smoke", config, target=20)
38
+
39
+ # Phase 2: Functional Tests (20% - 40%)
40
+ self._run_phase("functional", config, target=40)
41
+
42
+ # Phase 3: Integration Tests (40% - 60%)
43
+ self._run_phase("integration", config, target=60)
44
+
45
+ # Phase 4: Performance & Security (60% - 80%)
46
+ self._run_phase("performance", config, target=80)
47
+
48
+ # Phase 5: Advanced & Regression (80% - 90%)
49
+ if coverage_target >= 80:
50
+ self._run_phase("advanced", config, target=coverage_target)
51
+
52
+ self.results["duration"] = time.time() - self.start_time
53
+ self._save_results()
54
+
55
+ return self.results
56
+
57
+ def _run_phase(self, phase: str, config: Dict, target: int):
58
+ """Run a specific testing phase"""
59
+ phase_results = {
60
+ "total": 0,
61
+ "passed": 0,
62
+ "failed": 0,
63
+ "skipped": 0,
64
+ "coverage": 0,
65
+ }
66
+
67
+ # Execute tests based on configured engines
68
+ engines = config.get("engines", ["web"])
69
+
70
+ for engine_name in engines:
71
+ engine = self._get_engine(engine_name)
72
+ if engine:
73
+ result = engine.execute_phase(phase)
74
+ phase_results["total"] += result["total"]
75
+ phase_results["passed"] += result["passed"]
76
+ phase_results["failed"] += result["failed"]
77
+ phase_results["skipped"] += result["skipped"]
78
+
79
+ # Calculate coverage
80
+ if phase_results["total"] > 0:
81
+ phase_results["coverage"] = round(
82
+ (phase_results["passed"] / phase_results["total"]) * 100, 2
83
+ )
84
+
85
+ self.results["categories"][phase] = phase_results
86
+ self.coverage = self._calculate_total_coverage()
87
+ self.results["total_coverage"] = self.coverage
88
+
89
+ # Check if target reached
90
+ if self.coverage >= target:
91
+ console.print(f"[green]✓ Phase '{phase}' complete - Coverage: {self.coverage}%[/green]")
92
+
93
+ def _get_engine(self, engine_name: str):
94
+ """Get the appropriate test engine"""
95
+ from nextog.engines.web.browser import WebTestEngine
96
+ from nextog.engines.api.rest import APITestEngine
97
+ from nextog.engines.mobile.android import MobileTestEngine
98
+ from nextog.engines.embedded.hardware import EmbeddedTestEngine
99
+
100
+ engines = {
101
+ "web": WebTestEngine,
102
+ "api": APITestEngine,
103
+ "mobile": MobileTestEngine,
104
+ "embedded": EmbeddedTestEngine,
105
+ }
106
+
107
+ engine_class = engines.get(engine_name)
108
+ if engine_class:
109
+ return engine_class(self.settings, self.db, self.privacy)
110
+ return None
111
+
112
+ def _calculate_total_coverage(self) -> float:
113
+ """Calculate weighted total coverage"""
114
+ weights = {
115
+ "smoke": 0.10,
116
+ "functional": 0.25,
117
+ "integration": 0.25,
118
+ "performance": 0.20,
119
+ "advanced": 0.20,
120
+ }
121
+
122
+ total = 0.0
123
+ for phase, data in self.results["categories"].items():
124
+ weight = weights.get(phase, 0.1)
125
+ total += data["coverage"] * weight
126
+
127
+ return round(min(total, 90.0), 2) # Cap at 90%
128
+
129
+ def _save_results(self):
130
+ """Save results to local database (privacy-first)"""
131
+ self.db.save_test_results(self.results)
132
+ self.privacy.record_activity("test_run", self.results)
133
+
134
+
135
+ class TestSuite:
136
+ """Represents a collection of test cases"""
137
+
138
+ def __init__(self, name: str, suite_type: str):
139
+ self.name = name
140
+ self.suite_type = suite_type
141
+ self.test_cases: List[TestCase] = []
142
+ self.metadata: Dict[str, Any] = {}
143
+
144
+ def add_test(self, test_case: 'TestCase'):
145
+ self.test_cases.append(test_case)
146
+
147
+ def get_tests_by_priority(self, priority: str) -> List['TestCase']:
148
+ return [t for t in self.test_cases if t.priority == priority]
149
+
150
+ def get_coverage(self) -> float:
151
+ if not self.test_cases:
152
+ return 0.0
153
+ executed = sum(1 for t in self.test_cases if t.executed)
154
+ return round((executed / len(self.test_cases)) * 100, 2)
155
+
156
+
157
+ class TestCase:
158
+ """Individual test case"""
159
+
160
+ def __init__(self, name: str, test_type: str, element: str = ""):
161
+ self.name = name
162
+ self.test_type = test_type
163
+ self.element = element # The element being tested
164
+ self.description = ""
165
+ self.priority = "medium" # low, medium, high, critical
166
+ self.steps: List[Dict] = []
167
+ self.expected_result = ""
168
+ self.actual_result = ""
169
+ self.status = "pending" # pending, passed, failed, skipped
170
+ self.executed = False
171
+ self.duration = 0.0
172
+ self.screenshots: List[str] = []
173
+ self.metadata: Dict[str, Any] = {}
174
+
175
+ def execute(self) -> Dict:
176
+ """Execute this test case"""
177
+ self.executed = True
178
+ start = time.time()
179
+
180
+ try:
181
+ # Steps execution would be handled by specific engine
182
+ self.status = "passed"
183
+ self.actual_result = "All assertions passed"
184
+ except Exception as e:
185
+ self.status = "failed"
186
+ self.actual_result = str(e)
187
+
188
+ self.duration = time.time() - start
189
+ return {
190
+ "status": self.status,
191
+ "duration": self.duration,
192
+ "result": self.actual_result,
193
+ }
@@ -0,0 +1,129 @@
1
+ """
2
+ Permission Manager - Role-based access control for nextOG CLI
3
+ """
4
+
5
+ from typing import Dict, List, Optional
6
+ from datetime import datetime
7
+ from enum import Enum
8
+
9
+
10
+ class Role(Enum):
11
+ ADMIN = "admin"
12
+ TESTER = "tester"
13
+ VIEWER = "viewer"
14
+ CI = "ci"
15
+
16
+
17
+ # Permission matrix: role -> list of allowed actions
18
+ PERMISSION_MATRIX = {
19
+ Role.ADMIN: [
20
+ "test.run", "test.create", "test.delete", "test.view",
21
+ "user.create", "user.delete", "user.edit", "user.view",
22
+ "config.read", "config.write",
23
+ "data.export", "data.purge", "data.view",
24
+ "live.start", "live.manage",
25
+ "train.start", "train.stop", "train.view",
26
+ "report.generate", "report.view",
27
+ ],
28
+ Role.TESTER: [
29
+ "test.run", "test.create", "test.view",
30
+ "data.export", "data.view",
31
+ "live.start",
32
+ "train.view",
33
+ "report.generate", "report.view",
34
+ ],
35
+ Role.VIEWER: [
36
+ "test.view",
37
+ "data.view",
38
+ "report.view",
39
+ ],
40
+ Role.CI: [
41
+ "test.run", "test.view",
42
+ "report.generate", "report.view",
43
+ ],
44
+ }
45
+
46
+
47
+ class PermissionManager:
48
+ """Manages users, roles, and permissions"""
49
+
50
+ def __init__(self, db):
51
+ self.db = db
52
+ self._current_user = None
53
+
54
+ def create_user(self, username: str, role: str, email: Optional[str] = None) -> Dict:
55
+ """Create a new user with specified role"""
56
+ # Validate role
57
+ try:
58
+ role_enum = Role(role.lower())
59
+ except ValueError:
60
+ raise ValueError(f"Invalid role: {role}. Must be one of: {[r.value for r in Role]}")
61
+
62
+ user = {
63
+ "username": username,
64
+ "role": role_enum.value,
65
+ "email": email,
66
+ "status": "active",
67
+ "created_at": datetime.now().isoformat(),
68
+ "last_login": None,
69
+ }
70
+
71
+ self.db.save_user(user)
72
+ return user
73
+
74
+ def authenticate(self, username: str) -> bool:
75
+ """Authenticate user and set as current"""
76
+ user = self.db.get_user(username)
77
+ if user and user["status"] == "active":
78
+ self._current_user = user
79
+ self.db.update_last_login(username)
80
+ return True
81
+ return False
82
+
83
+ def check_permission(self, action: str) -> bool:
84
+ """Check if current user has permission for action"""
85
+ if not self._current_user:
86
+ return False
87
+
88
+ role = Role(self._current_user["role"])
89
+ allowed = PERMISSION_MATRIX.get(role, [])
90
+ return action in allowed
91
+
92
+ def require_permission(self, action: str):
93
+ """Raise error if current user lacks permission"""
94
+ if not self.check_permission(action):
95
+ raise PermissionError(
96
+ f"User '{self._current_user.get('username', 'unknown')}' "
97
+ f"lacks permission for '{action}'"
98
+ )
99
+
100
+ def list_users(self) -> List[Dict]:
101
+ """List all users"""
102
+ return self.db.get_all_users()
103
+
104
+ def update_role(self, username: str, new_role: str):
105
+ """Update user's role"""
106
+ try:
107
+ Role(new_role.lower())
108
+ except ValueError:
109
+ raise ValueError(f"Invalid role: {new_role}")
110
+
111
+ self.db.update_user_role(username, new_role)
112
+
113
+ def delete_user(self, username: str):
114
+ """Delete a user"""
115
+ self.require_permission("user.delete")
116
+ self.db.delete_user(username)
117
+
118
+ def get_user_permissions(self, username: str) -> List[str]:
119
+ """Get all permissions for a user"""
120
+ user = self.db.get_user(username)
121
+ if not user:
122
+ return []
123
+
124
+ role = Role(user["role"])
125
+ return PERMISSION_MATRIX.get(role, [])
126
+
127
+ @property
128
+ def current_user(self) -> Optional[Dict]:
129
+ return self._current_user
nextog/core/privacy.py ADDED
@@ -0,0 +1,130 @@
1
+ """
2
+ Privacy Engine - Ensures all data stays local with encryption
3
+ Privacy-first architecture: NO external data transfer
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import hashlib
9
+ from typing import Dict, Any, Optional
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+ from cryptography.fernet import Fernet
13
+
14
+
15
+ class PrivacyEngine:
16
+ """Privacy-first data management - all data stays local"""
17
+
18
+ def __init__(self, db):
19
+ self.db = db
20
+ self.data_dir = Path.home() / ".nextog" / "data"
21
+ self.data_dir.mkdir(parents=True, exist_ok=True)
22
+ self._encryption_key = self._load_or_create_key()
23
+ self._cipher = Fernet(self._encryption_key)
24
+ self._external_sync_enabled = False # NEVER enable external sync
25
+
26
+ def _load_or_create_key(self) -> bytes:
27
+ """Load encryption key or create new one"""
28
+ key_path = self.data_dir / ".encryption_key"
29
+
30
+ if key_path.exists():
31
+ return key_path.read_bytes()
32
+
33
+ key = Fernet.generate_key()
34
+ key_path.write_bytes(key)
35
+ # Set restrictive permissions
36
+ os.chmod(key_path, 0o600)
37
+ return key
38
+
39
+ def encrypt_data(self, data: Any) -> bytes:
40
+ """Encrypt data using Fernet (AES-128-CBC)"""
41
+ json_str = json.dumps(data, default=str)
42
+ return self._cipher.encrypt(json_str.encode())
43
+
44
+ def decrypt_data(self, encrypted: bytes) -> Any:
45
+ """Decrypt data"""
46
+ decrypted = self._cipher.decrypt(encrypted)
47
+ return json.loads(decrypted.decode())
48
+
49
+ def record_activity(self, activity_type: str, data: Dict):
50
+ """Record user activity locally (encrypted)"""
51
+ record = {
52
+ "type": activity_type,
53
+ "data": data,
54
+ "timestamp": datetime.now().isoformat(),
55
+ "hash": hashlib.sha256(json.dumps(data, default=str).encode()).hexdigest(),
56
+ }
57
+
58
+ encrypted = self.encrypt_data(record)
59
+ activity_file = self.data_dir / "activity.log.enc"
60
+
61
+ with open(activity_file, "ab") as f:
62
+ # Write length-prefixed encrypted record
63
+ f.write(len(encrypted).to_bytes(4, "big"))
64
+ f.write(encrypted)
65
+
66
+ def export_data(self, output_path: str, encrypt: bool = True):
67
+ """Export all local data for user backup"""
68
+ all_data = self.db.export_all()
69
+
70
+ if encrypt:
71
+ encrypted = self.encrypt_data(all_data)
72
+ Path(output_path).write_bytes(encrypted)
73
+ else:
74
+ Path(output_path).write_text(json.dumps(all_data, indent=2, default=str))
75
+
76
+ def purge_all_data(self):
77
+ """Delete ALL local data permanently"""
78
+ # Purge database
79
+ self.db.purge_all()
80
+
81
+ # Purge data directory
82
+ import shutil
83
+ if self.data_dir.exists():
84
+ for item in self.data_dir.iterdir():
85
+ if item.name != ".encryption_key":
86
+ if item.is_file():
87
+ item.unlink()
88
+ else:
89
+ shutil.rmtree(item)
90
+
91
+ def get_storage_stats(self) -> Dict:
92
+ """Get storage statistics"""
93
+ db_size = 0
94
+ db_path = self.data_dir / "nextog.db"
95
+ if db_path.exists():
96
+ db_size = db_path.stat().st_size
97
+
98
+ test_count = self.db.count_test_results()
99
+ pattern_count = self.db.count_patterns()
100
+
101
+ # Calculate directory size
102
+ total_size = sum(f.stat().st_size for f in self.data_dir.rglob("*") if f.is_file())
103
+
104
+ return {
105
+ "db_size": self._format_size(db_size),
106
+ "total_size": self._format_size(total_size),
107
+ "test_count": test_count,
108
+ "pattern_count": pattern_count,
109
+ "encrypted": True,
110
+ "external_sync": self._external_sync_enabled, # Always False
111
+ }
112
+
113
+ def _format_size(self, size_bytes: int) -> str:
114
+ """Format file size"""
115
+ for unit in ["B", "KB", "MB", "GB"]:
116
+ if size_bytes < 1024:
117
+ return f"{size_bytes:.1f} {unit}"
118
+ size_bytes /= 1024
119
+ return f"{size_bytes:.1f} TB"
120
+
121
+ def get_data_hash(self) -> str:
122
+ """Get hash of all data for integrity verification"""
123
+ all_data = self.db.export_all()
124
+ return hashlib.sha256(json.dumps(all_data, sort_keys=True, default=str).encode()).hexdigest()
125
+
126
+ def verify_integrity(self) -> bool:
127
+ """Verify data integrity hasn't been tampered with"""
128
+ stored_hash = self.db.get_metadata("data_hash")
129
+ current_hash = self.get_data_hash()
130
+ return stored_hash == current_hash
@@ -0,0 +1,204 @@
1
+ """
2
+ Coverage Reporter - Track and display test coverage
3
+ """
4
+
5
+ import json
6
+ from typing import Dict, List, Optional, Any
7
+ from pathlib import Path
8
+ from datetime import datetime
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from rich.panel import Panel
12
+ from rich.progress import BarColumn, Progress, TextColumn
13
+
14
+ console = Console()
15
+
16
+
17
+ class CoverageReporter:
18
+ """Track and report test coverage from 0% to 90%"""
19
+
20
+ COVERAGE_PHASES = [
21
+ {"name": "Smoke Tests", "range": (0, 20), "weight": 0.10, "description": "Basic element detection & smoke tests"},
22
+ {"name": "Functional Tests", "range": (20, 40), "weight": 0.25, "description": "Functional tests & API validation"},
23
+ {"name": "Integration Tests", "range": (40, 60), "weight": 0.25, "description": "Integration tests & edge cases"},
24
+ {"name": "Performance & Security", "range": (60, 80), "weight": 0.20, "description": "Performance & security tests"},
25
+ {"name": "Advanced & Regression", "range": (80, 90), "weight": 0.20, "description": "Advanced scenarios & regression suites"},
26
+ ]
27
+
28
+ def __init__(self, db, settings):
29
+ self.db = db
30
+ self.settings = settings
31
+
32
+ def get_coverage(self, project: Optional[str] = None) -> Dict:
33
+ """Get coverage data for a project"""
34
+ results = self.db.get_latest_results(project)
35
+
36
+ coverage_data = {
37
+ "total_coverage": 0.0,
38
+ "phases": {},
39
+ "elements_covered": 0,
40
+ "elements_total": 0,
41
+ "timestamp": datetime.now().isoformat(),
42
+ "project": project,
43
+ }
44
+
45
+ if results:
46
+ coverage_data["total_coverage"] = results.get("total_coverage", 0)
47
+
48
+ for phase_config in self.COVERAGE_PHASES:
49
+ phase_name = phase_config["name"].lower().split()[0]
50
+ phase_data = results.get("categories", {}).get(phase_name, {})
51
+ coverage_data["phases"][phase_config["name"]] = {
52
+ "coverage": phase_data.get("coverage", 0),
53
+ "target_range": f"{phase_config['range'][0]}-{phase_config['range'][1]}%",
54
+ "description": phase_config["description"],
55
+ "status": self._get_phase_status(phase_data.get("coverage", 0), phase_config["range"]),
56
+ }
57
+
58
+ return coverage_data
59
+
60
+ def display_coverage(self, data: Dict):
61
+ """Display coverage in a rich table"""
62
+ console.print()
63
+
64
+ # Overall coverage bar
65
+ total = data.get("total_coverage", 0)
66
+ color = "red" if total < 40 else "yellow" if total < 70 else "green"
67
+
68
+ console.print(Panel(
69
+ f"[bold {color}]{total}%[/bold {color}] Coverage",
70
+ title="📊 Total Coverage",
71
+ border_style=color,
72
+ ))
73
+
74
+ # Phase breakdown
75
+ table = Table(title="Coverage by Phase")
76
+ table.add_column("Phase", style="cyan", width=25)
77
+ table.add_column("Coverage", style="white", width=10)
78
+ table.add_column("Target", style="dim", width=15)
79
+ table.add_column("Status", style="white", width=10)
80
+ table.add_column("Description", style="dim")
81
+
82
+ for phase_name, phase_data in data.get("phases", {}).items():
83
+ coverage = phase_data["coverage"]
84
+ status_emoji = "✅" if phase_data["status"] == "complete" else "🔄" if phase_data["status"] == "in_progress" else "⬜"
85
+
86
+ table.add_row(
87
+ phase_name,
88
+ f"{coverage}%",
89
+ phase_data["target_range"],
90
+ f"{status_emoji} {phase_data['status']}",
91
+ phase_data["description"],
92
+ )
93
+
94
+ console.print(table)
95
+
96
+ # Progress bar visualization
97
+ console.print()
98
+ with Progress(
99
+ TextColumn("[bold blue]Overall Progress"),
100
+ BarColumn(bar_width=50),
101
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
102
+ console=console,
103
+ ) as progress:
104
+ task = progress.add_task("", total=90, completed=total)
105
+
106
+ def generate_report(self, data: Dict, format: str = "html") -> str:
107
+ """Generate a coverage report"""
108
+ report_dir = Path(".nextog/reports")
109
+ report_dir.mkdir(parents=True, exist_ok=True)
110
+
111
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
112
+
113
+ if format == "json":
114
+ output_path = report_dir / f"coverage_{timestamp}.json"
115
+ output_path.write_text(json.dumps(data, indent=2, default=str))
116
+
117
+ elif format == "html":
118
+ output_path = report_dir / f"coverage_{timestamp}.html"
119
+ html = self._generate_html_report(data)
120
+ output_path.write_text(html)
121
+
122
+ elif format == "pdf":
123
+ output_path = report_dir / f"coverage_{timestamp}.pdf"
124
+ # PDF generation would use weasyprint or similar
125
+ html = self._generate_html_report(data)
126
+ output_path.write_text(html) # Fallback to HTML
127
+
128
+ else:
129
+ output_path = report_dir / f"coverage_{timestamp}.txt"
130
+ output_path.write_text(self._generate_text_report(data))
131
+
132
+ return str(output_path)
133
+
134
+ def _get_phase_status(self, coverage: float, target_range: tuple) -> str:
135
+ """Determine phase status"""
136
+ if coverage >= target_range[1]:
137
+ return "complete"
138
+ elif coverage >= target_range[0]:
139
+ return "in_progress"
140
+ return "pending"
141
+
142
+ def _generate_html_report(self, data: Dict) -> str:
143
+ """Generate HTML report"""
144
+ total = data.get("total_coverage", 0)
145
+ phases_html = ""
146
+
147
+ for phase_name, phase_data in data.get("phases", {}).items():
148
+ coverage = phase_data["coverage"]
149
+ color = "#22c55e" if coverage >= 60 else "#eab308" if coverage >= 30 else "#ef4444"
150
+ phases_html += f"""
151
+ <div style="margin: 10px 0; padding: 15px; background: #1e293b; border-radius: 8px;">
152
+ <div style="display: flex; justify-content: space-between;">
153
+ <strong>{phase_name}</strong>
154
+ <span>{coverage}% / {phase_data['target_range']}</span>
155
+ </div>
156
+ <div style="background: #334155; border-radius: 4px; margin-top: 8px;">
157
+ <div style="background: {color}; height: 8px; border-radius: 4px; width: {coverage}%"></div>
158
+ </div>
159
+ <p style="color: #94a3b8; font-size: 0.85rem; margin-top: 5px;">{phase_data['description']}</p>
160
+ </div>
161
+ """
162
+
163
+ return f"""<!DOCTYPE html>
164
+ <html>
165
+ <head>
166
+ <title>nextOG Coverage Report</title>
167
+ <style>
168
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 40px; }}
169
+ .container {{ max-width: 800px; margin: 0 auto; }}
170
+ h1 {{ color: #38bdf8; }}
171
+ .summary {{ font-size: 2rem; text-align: center; padding: 30px; }}
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <div class="container">
176
+ <h1>🚀 nextOG Coverage Report</h1>
177
+ <p>Generated: {data.get('timestamp', 'N/A')}</p>
178
+ <div class="summary">
179
+ <h2>Total Coverage: {total}%</h2>
180
+ </div>
181
+ <h3>Phase Breakdown</h3>
182
+ {phases_html}
183
+ </div>
184
+ </body>
185
+ </html>"""
186
+
187
+ def _generate_text_report(self, data: Dict) -> str:
188
+ """Generate plain text report"""
189
+ lines = [
190
+ "=" * 60,
191
+ "nextOG Coverage Report",
192
+ "=" * 60,
193
+ f"Generated: {data.get('timestamp', 'N/A')}",
194
+ f"Total Coverage: {data.get('total_coverage', 0)}%",
195
+ "",
196
+ "Phase Breakdown:",
197
+ "-" * 40,
198
+ ]
199
+
200
+ for phase_name, phase_data in data.get("phases", {}).items():
201
+ lines.append(f" {phase_name}: {phase_data['coverage']}% ({phase_data['target_range']})")
202
+
203
+ lines.append("=" * 60)
204
+ return "\n".join(lines)