| |
| """ |
| Coverage Trend Tracking System for Atom v5.0 |
| |
| This script tracks coverage changes over time, establishing a baseline and |
| monitoring progress toward the 80% target. It records snapshots, calculates |
| deltas, maintains historical data, and generates trend visualizations. |
| |
| Usage: |
| # Record current coverage with commit hash |
| python coverage_trend_tracker.py --commit <hash> --chart |
| |
| # Record from specific coverage file |
| python coverage_trend_tracker.py --coverage-file path/to/coverage.json --commit <hash> |
| |
| # Check for regressions (CI usage) |
| python coverage_trend_tracker.py --regression-check |
| |
| # Compare two commits |
| python coverage_trend_tracker.py --compare-commits <hash1> <hash2> |
| |
| # Forecast when 80% target will be reached |
| python coverage_trend_tracker.py --forecast 80 |
| |
| # Record coverage for CI (generates PR comment payload) |
| python coverage_trend_tracker.py --ci-record |
| |
| Features: |
| - Per-commit coverage tracking with automatic git hash detection |
| - Baseline establishment for v5.0 expansion |
| - Delta calculation (absolute and relative changes) |
| - Historical trend data (last 30 entries) |
| - ASCII visualization with 80% target marker |
| - Regression detection (alerts on >1% decrease) |
| - Timeline forecasting for 80% target |
| - CI integration hooks for PR comments |
| """ |
|
|
| import argparse |
| import json |
| import subprocess |
| import sys |
| from datetime import datetime, timezone |
| from pathlib import Path |
| from typing import Dict, Any, List, Optional, Tuple |
|
|
|
|
| |
| DEFAULT_COVERAGE_FILE = Path("tests/coverage_reports/metrics/coverage.json") |
| DEFAULT_TREND_FILE = Path("tests/coverage_reports/metrics/coverage_trend_v5.0.json") |
| DEFAULT_TRENDS_DIR = Path("tests/coverage_reports/trends") |
| TARGET_COVERAGE = 80.0 |
| REGRESSION_THRESHOLD = 1.0 |
|
|
|
|
| def get_git_commit_hash() -> Optional[str]: |
| """ |
| Get current git commit hash. |
| |
| Returns: |
| Commit hash as string, or None if not in git repo |
| """ |
| try: |
| result = subprocess.run( |
| ["git", "rev-parse", "HEAD"], |
| capture_output=True, |
| text=True, |
| check=True |
| ) |
| return result.stdout.strip() |
| except (subprocess.CalledProcessError, FileNotFoundError): |
| return None |
|
|
|
|
| def get_git_commit_message(commit_hash: str) -> Optional[str]: |
| """ |
| Get git commit message for a given hash. |
| |
| Args: |
| commit_hash: Git commit hash |
| |
| Returns: |
| Commit message subject line, or None if not available |
| """ |
| try: |
| result = subprocess.run( |
| ["git", "log", "-1", "--format=%s", commit_hash], |
| capture_output=True, |
| text=True, |
| check=True |
| ) |
| return result.stdout.strip() |
| except (subprocess.CalledProcessError, FileNotFoundError): |
| return None |
|
|
|
|
| def record_snapshot(coverage_data: Dict[str, Any], commit_hash: Optional[str] = None) -> Dict[str, Any]: |
| """ |
| Extract coverage snapshot from coverage data. |
| |
| Args: |
| coverage_data: Loaded coverage.json data |
| commit_hash: Git commit hash (auto-detected if None) |
| |
| Returns: |
| Snapshot dict with timestamp, commit, coverage metrics |
| """ |
| |
| totals = coverage_data.get("totals", {}) |
| overall_coverage = totals.get("percent_covered", 0.0) |
| covered_lines = totals.get("covered_lines", 0) |
| total_lines = totals.get("num_statements", 0) |
| covered_branches = totals.get("covered_branches", 0) |
| total_branches = totals.get("num_branches", 0) |
|
|
| |
| files = coverage_data.get("files", {}) |
| module_breakdown = {} |
|
|
| for file_path, file_data in files.items(): |
| |
| if file_path.startswith("core/"): |
| module = "core" |
| elif file_path.startswith("api/"): |
| module = "api" |
| elif file_path.startswith("tools/"): |
| module = "tools" |
| elif file_path.startswith("skills/"): |
| module = "skills" |
| else: |
| module = "other" |
|
|
| |
| if module not in module_breakdown: |
| module_breakdown[module] = {"covered": 0, "total": 0} |
|
|
| summary = file_data.get("summary", {}) |
| module_breakdown[module]["covered"] += summary.get("covered_lines", 0) |
| module_breakdown[module]["total"] += summary.get("num_statements", 0) |
|
|
| |
| module_percentages = {} |
| for module, data in module_breakdown.items(): |
| if data["total"] > 0: |
| module_percentages[module] = (data["covered"] / data["total"]) * 100 |
| else: |
| module_percentages[module] = 0.0 |
|
|
| |
| if commit_hash is None: |
| commit_hash = get_git_commit_hash() |
|
|
| |
| commit_message = None |
| if commit_hash: |
| commit_message = get_git_commit_message(commit_hash) |
|
|
| |
| snapshot = { |
| "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), |
| "commit": commit_hash, |
| "commit_message": commit_message, |
| "overall_coverage": round(overall_coverage, 2), |
| "covered_lines": covered_lines, |
| "total_lines": total_lines, |
| "branch_coverage": round((covered_branches / total_branches * 100) if total_branches > 0 else 0.0, 2), |
| "covered_branches": covered_branches, |
| "total_branches": total_branches, |
| "module_breakdown": { |
| module: round(pct, 2) |
| for module, pct in module_percentages.items() |
| } |
| } |
|
|
| return snapshot |
|
|
|
|
| def get_trend_history(trend_file: Path = DEFAULT_TREND_FILE) -> Dict[str, Any]: |
| """ |
| Load trend history from file, creating new structure if needed. |
| |
| Args: |
| trend_file: Path to trend JSON file |
| |
| Returns: |
| Trend data dict with baseline, history, current, metadata |
| """ |
| |
| trend_data = { |
| "baseline": None, |
| "history": [], |
| "current": None, |
| "metadata": { |
| "version": "5.0", |
| "target_coverage": TARGET_COVERAGE, |
| "max_history_entries": 30, |
| "created_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") |
| } |
| } |
|
|
| |
| if trend_file.exists(): |
| try: |
| with open(trend_file, 'r') as f: |
| loaded_data = json.load(f) |
| |
| trend_data.update(loaded_data) |
| except (json.JSONDecodeError, IOError) as e: |
| print(f"Warning: Could not load trend file: {e}", file=sys.stderr) |
|
|
| return trend_data |
|
|
|
|
| def calculate_delta(current: float, previous: float) -> Dict[str, Any]: |
| """ |
| Calculate delta between two coverage values. |
| |
| Args: |
| current: Current coverage percentage |
| previous: Previous coverage percentage |
| |
| Returns: |
| Dict with absolute_change, relative_change, direction |
| """ |
| absolute_change = current - previous |
| relative_change = (absolute_change / previous * 100) if previous > 0 else 0.0 |
|
|
| if absolute_change > 0: |
| direction = "increase" |
| elif absolute_change < 0: |
| direction = "decrease" |
| else: |
| direction = "no_change" |
|
|
| return { |
| "absolute_change": round(absolute_change, 2), |
| "relative_change": round(relative_change, 2), |
| "direction": direction |
| } |
|
|
|
|
| def update_trend_data(snapshot: Dict[str, Any], trend_data: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| Update trend data with new snapshot. |
| |
| Args: |
| snapshot: Coverage snapshot to add |
| trend_data: Existing trend data |
| |
| Returns: |
| Updated trend data |
| """ |
| |
| if trend_data["history"]: |
| previous_coverage = trend_data["history"][-1]["overall_coverage"] |
| snapshot["delta"] = calculate_delta(snapshot["overall_coverage"], previous_coverage) |
| else: |
| snapshot["delta"] = { |
| "absolute_change": 0.0, |
| "relative_change": 0.0, |
| "direction": "baseline" |
| } |
|
|
| |
| trend_data["history"].append(snapshot) |
|
|
| |
| if len(trend_data["history"]) > trend_data["metadata"]["max_history_entries"]: |
| trend_data["history"] = trend_data["history"][-trend_data["metadata"]["max_history_entries"]:] |
|
|
| |
| trend_data["current"] = snapshot |
|
|
| |
| if trend_data["baseline"] is None: |
| trend_data["baseline"] = snapshot |
|
|
| |
| trend_data["metadata"]["last_updated"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") |
| trend_data["metadata"]["total_snapshots"] = len(trend_data["history"]) |
|
|
| return trend_data |
|
|
|
|
| def write_trend_data( |
| trend_data: Dict[str, Any], |
| trend_file: Path = DEFAULT_TREND_FILE, |
| trends_dir: Path = DEFAULT_TRENDS_DIR |
| ) -> None: |
| """ |
| Write trend data to main file and create daily snapshot. |
| |
| Args: |
| trend_data: Trend data to write |
| trend_file: Path to main trend file |
| trends_dir: Path to daily snapshots directory |
| """ |
| |
| trend_file.parent.mkdir(parents=True, exist_ok=True) |
| trends_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| with open(trend_file, 'w') as f: |
| json.dump(trend_data, f, indent=2) |
|
|
| |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") |
| daily_snapshot_path = trends_dir / f"{today}_coverage_trend.json" |
|
|
| with open(daily_snapshot_path, 'w') as f: |
| json.dump(trend_data, f, indent=2) |
|
|
| print(f"✅ Trend data saved to: {trend_file}") |
| print(f"✅ Daily snapshot saved to: {daily_snapshot_path}") |
|
|
|
|
| def generate_visualization(trend_data: Dict[str, Any], width: int = 60) -> str: |
| """ |
| Generate ASCII chart showing coverage trend over time. |
| |
| Args: |
| trend_data: Trend data with history |
| width: Chart width in characters |
| |
| Returns: |
| Formatted ASCII chart string |
| """ |
| if not trend_data["history"]: |
| return "No trend data available yet." |
|
|
| lines = [] |
| lines.append("") |
| lines.append("=" * 80) |
| lines.append("COVERAGE TREND VISUALIZATION") |
| lines.append("=" * 80) |
| lines.append("") |
|
|
| |
| current = trend_data["current"]["overall_coverage"] |
| baseline = trend_data["baseline"]["overall_coverage"] |
|
|
| |
| lines.append(f"Baseline: {baseline:6.2f}%") |
| lines.append(f"Current: {current:6.2f}%") |
| lines.append(f"Target: {TARGET_COVERAGE:6.2f}%") |
| lines.append("") |
|
|
| |
| if "delta" in trend_data["current"]: |
| delta = trend_data["current"]["delta"] |
| direction_symbol = { |
| "increase": "↑", |
| "decrease": "↓", |
| "no_change": "→", |
| "baseline": "=" |
| }.get(delta["direction"], "?") |
|
|
| lines.append(f"Change: {direction_symbol} {delta['absolute_change']:+6.2f}% ({delta['relative_change']:+.2f}% relative)") |
| lines.append("") |
|
|
| |
| lines.append("Coverage History (last 30 snapshots):") |
| lines.append("") |
|
|
| |
| coverages = [s["overall_coverage"] for s in trend_data["history"]] |
| min_cov = min(coverages) |
| max_cov = max(coverages) |
|
|
| |
| if min_cov < TARGET_COVERAGE < max_cov: |
| max_cov = max(max_cov, TARGET_COVERAGE) |
|
|
| |
| chart_height = 20 |
| range_cov = max_cov - min_cov if max_cov > min_cov else 1.0 |
|
|
| |
| for row in range(chart_height, -1, -1): |
| value = min_cov + (range_cov * row / chart_height) |
|
|
| |
| label = f"{value:5.1f}%" |
|
|
| |
| chart_row = label + " |" |
|
|
| |
| for snapshot in trend_data["history"]: |
| cov = snapshot["overall_coverage"] |
|
|
| |
| if abs(cov - value) < (range_cov / chart_height): |
| |
| if snapshot == trend_data["baseline"]: |
| chart_row += "B" |
| elif snapshot == trend_data["current"]: |
| chart_row += "C" |
| else: |
| chart_row += "*" |
| else: |
| chart_row += " " |
|
|
| chart_row += "|" |
|
|
| |
| if abs(TARGET_COVERAGE - value) < (range_cov / chart_height): |
| chart_row += " <-- TARGET (80%)" |
|
|
| lines.append(chart_row) |
|
|
| |
| lines.append(" +" + "-" * width + "+") |
| lines.append("") |
| lines.append("Legend: B = Baseline, C = Current, * = Historical snapshot") |
| lines.append("") |
|
|
| |
| lines.append("Recent Snapshots:") |
| lines.append("") |
| lines.append(f"{'Timestamp':<25} {'Commit':<12} {'Coverage':>8} {'Change':>8}") |
| lines.append("-" * 60) |
|
|
| for snapshot in trend_data["history"][-10:]: |
| timestamp = snapshot["timestamp"][:19].replace("T", " ") |
| commit = (snapshot["commit"] or "unknown")[:10] |
| coverage = f"{snapshot['overall_coverage']:.2f}%" |
|
|
| if "delta" in snapshot: |
| delta = snapshot["delta"]["absolute_change"] |
| change = f"{delta:+.2f}%" |
| else: |
| change = "N/A" |
|
|
| lines.append(f"{timestamp:<25} {commit:<12} {coverage:>8} {change:>8}") |
|
|
| lines.append("") |
| lines.append("=" * 80) |
|
|
| return "\n".join(lines) |
|
|
|
|
| def check_regression(trend_data: Dict[str, Any], threshold: float = REGRESSION_THRESHOLD) -> Tuple[bool, List[str]]: |
| """ |
| Check for coverage regression against recent history. |
| |
| Args: |
| trend_data: Trend data with history |
| threshold: Regression threshold (percentage points) |
| |
| Returns: |
| Tuple of (has_regression, list of regression messages) |
| """ |
| if len(trend_data["history"]) < 2: |
| return False, ["Insufficient history for regression check"] |
|
|
| messages = [] |
| current = trend_data["current"]["overall_coverage"] |
|
|
| |
| compare_count = min(3, len(trend_data["history"]) - 1) |
| has_regression = False |
|
|
| for i in range(1, compare_count + 1): |
| previous = trend_data["history"][-(i + 1)] |
| previous_coverage = previous["overall_coverage"] |
| delta = current - previous_coverage |
|
|
| if delta < -threshold: |
| has_regression = True |
| commit_msg = previous.get("commit_message", "")[:50] |
| messages.append( |
| f"REGRESSION: Coverage decreased by {delta:.2f}% " |
| f"since {previous['timestamp'][:10]} " |
| f"(commit {previous['commit'][:8] if previous.get('commit') else 'unknown'}: '{commit_msg}')" |
| ) |
|
|
| |
| if trend_data["history"][-1].get("module_breakdown") and trend_data["current"].get("module_breakdown"): |
| current_modules = trend_data["current"]["module_breakdown"] |
| previous_modules = trend_data["history"][-1]["module_breakdown"] |
|
|
| for module in current_modules: |
| if module in previous_modules: |
| delta = current_modules[module] - previous_modules[module] |
| if delta < -threshold: |
| has_regression = True |
| messages.append( |
| f"MODULE REGRESSION: {module} decreased by {delta:.2f}% " |
| f"({previous_modules[module]:.2f}% → {current_modules[module]:.2f}%)" |
| ) |
|
|
| if not has_regression: |
| messages.append(f"No regression detected (coverage stable or improving)") |
|
|
| return has_regression, messages |
|
|
|
|
| def forecast_target(trend_data: Dict[str, Any], target: float = TARGET_COVERAGE) -> str: |
| """ |
| Forecast when target coverage will be reached based on trend. |
| |
| Args: |
| trend_data: Trend data with history |
| target: Target coverage percentage |
| |
| Returns: |
| Formatted forecast string |
| """ |
| if len(trend_data["history"]) < 3: |
| return "Insufficient history for forecasting (need at least 3 snapshots)" |
|
|
| current = trend_data["current"]["overall_coverage"] |
|
|
| if current >= target: |
| return f"Target {target}% already reached! Current: {current:.2f}%" |
|
|
| |
| recent_snapshots = trend_data["history"][-5:] |
| increases = [] |
|
|
| for i in range(1, len(recent_snapshots)): |
| delta = recent_snapshots[i]["overall_coverage"] - recent_snapshots[i - 1]["overall_coverage"] |
| increases.append(delta) |
|
|
| avg_increase = sum(increases) / len(increases) if increases else 0 |
|
|
| if avg_increase <= 0: |
| return ( |
| f"Cannot forecast: Coverage trend is flat or decreasing " |
| f"(avg change: {avg_increase:.2f}% per snapshot)" |
| ) |
|
|
| |
| remaining = target - current |
| snapshots_needed = int(remaining / avg_increase) + 1 |
|
|
| |
| if len(trend_data["history"]) >= 2: |
| first_snapshot = datetime.fromisoformat(trend_data["history"][0]["timestamp"].replace("Z", "+00:00")) |
| last_snapshot = datetime.fromisoformat(trend_data["current"]["timestamp"].replace("Z", "+00:00")) |
| days_span = (last_snapshot - first_snapshot).days |
| days_per_snapshot = days_span / (len(trend_data["history"]) - 1) if len(trend_data["history"]) > 1 else 1 |
|
|
| estimated_days = snapshots_needed * days_per_snapshot |
| from datetime import timedelta |
| estimated_date = last_snapshot + timedelta(days=estimated_days) |
|
|
| |
| optimistic_days = estimated_days * 0.7 |
| realistic_days = estimated_days |
| pessimistic_days = estimated_days * 1.3 |
|
|
| optimistic_date = last_snapshot + timedelta(days=optimistic_days) |
| pessimistic_date = last_snapshot + timedelta(days=pessimistic_days) |
|
|
| lines = [] |
| lines.append("") |
| lines.append("=" * 80) |
| lines.append(f"COVERAGE FORECAST: {target}% TARGET") |
| lines.append("=" * 80) |
| lines.append("") |
| lines.append(f"Current Coverage: {current:.2f}%") |
| lines.append(f"Target Coverage: {target:.2f}%") |
| lines.append(f"Remaining: {remaining:.2f}%") |
| lines.append("") |
| lines.append(f"Average Increase: {avg_increase:.2f}% per snapshot") |
| lines.append(f"Snapshot Frequency: ~{days_per_snapshot:.1f} days per snapshot") |
| lines.append("") |
| lines.append(f"Snapshots Needed: ~{snapshots_needed} snapshots") |
| lines.append(f"Estimated Timeline: ~{estimated_days:.0f} days") |
| lines.append("") |
| lines.append("Scenarios:") |
| lines.append(f" Optimistic: {optimistic_date.strftime('%Y-%m-%d')} ({optimistic_days:.0f} days)") |
| lines.append(f" Realistic: {estimated_date.strftime('%Y-%m-%d')} ({realistic_days:.0f} days)") |
| lines.append(f" Pessimistic: {pessimistic_date.strftime('%Y-%m-%d')} ({pessimistic_days:.0f} days)") |
| lines.append("") |
| lines.append("=" * 80) |
|
|
| return "\n".join(lines) |
| else: |
| return f"~{snapshots_needed} snapshots needed (timeline estimation requires more history)" |
|
|
|
|
| def record_coverage_ci() -> Dict[str, Any]: |
| """ |
| Record coverage snapshot for CI/CD integration. |
| |
| Returns: |
| PR comment payload dict |
| """ |
| |
| if not DEFAULT_COVERAGE_FILE.exists(): |
| return {"error": "Coverage file not found"} |
|
|
| with open(DEFAULT_COVERAGE_FILE) as f: |
| coverage_data = json.load(f) |
|
|
| |
| commit_hash = get_git_commit_hash() |
|
|
| |
| snapshot = record_snapshot(coverage_data, commit_hash) |
| trend_data = get_trend_history() |
| trend_data = update_trend_data(snapshot, trend_data) |
| write_trend_data(trend_data) |
|
|
| |
| current = snapshot["overall_coverage"] |
| baseline = trend_data["baseline"]["overall_coverage"] |
| delta = current - baseline |
|
|
| payload = { |
| "title": "Coverage Report", |
| "summary": { |
| "current": f"{current:.2f}%", |
| "baseline": f"{baseline:.2f}%", |
| "delta": f"{delta:+.2f}%", |
| "target": f"{TARGET_COVERAGE:.2f}%" |
| }, |
| "metrics": { |
| "lines_covered": snapshot["covered_lines"], |
| "total_lines": snapshot["total_lines"], |
| "branch_coverage": snapshot["branch_coverage"] |
| }, |
| "modules": snapshot.get("module_breakdown", {}), |
| "trend": "increasing" if delta > 0 else "stable" if delta == 0 else "decreasing", |
| "commit": commit_hash[:8] if commit_hash else "unknown" |
| } |
|
|
| return payload |
|
|
|
|
| def main(): |
| """Main execution entry point.""" |
| parser = argparse.ArgumentParser( |
| description="Track coverage trends for Atom v5.0", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| Examples: |
| # Record current coverage with visualization |
| python coverage_trend_tracker.py --commit $(git rev-parse HEAD) --chart |
| |
| # Check for regressions (CI usage) |
| python coverage_trend_tracker.py --regression-check |
| |
| # Compare two commits |
| python coverage_trend_tracker.py --compare-commits abc123 def456 |
| |
| # Forecast when 80% will be reached |
| python coverage_trend_tracker.py --forecast 80 |
| |
| # Record for CI (generates PR comment payload) |
| python coverage_trend_tracker.py --ci-record |
| """ |
| ) |
|
|
| parser.add_argument( |
| "--coverage-file", |
| type=Path, |
| default=DEFAULT_COVERAGE_FILE, |
| help="Path to coverage.json (default: backend/tests/coverage_reports/metrics/coverage.json)" |
| ) |
| parser.add_argument( |
| "--output", |
| type=Path, |
| default=DEFAULT_TREND_FILE, |
| help="Output path for trend data (default: backend/tests/coverage_reports/metrics/coverage_trend_v5.0.json)" |
| ) |
| parser.add_argument( |
| "--commit", |
| type=str, |
| default=None, |
| help="Git commit hash (auto-detected if not provided)" |
| ) |
| parser.add_argument( |
| "--chart", |
| action="store_true", |
| help="Generate ASCII visualization chart" |
| ) |
| parser.add_argument( |
| "--regression-check", |
| action="store_true", |
| help="Check for coverage regressions (exits with 1 on regression)" |
| ) |
| parser.add_argument( |
| "--compare-commits", |
| nargs=2, |
| metavar=("COMMIT1", "COMMIT2"), |
| help="Compare coverage between two commits" |
| ) |
| parser.add_argument( |
| "--forecast", |
| type=float, |
| metavar="TARGET", |
| default=None, |
| help="Forecast when TARGET coverage will be reached (default: 80.0%%)" |
| ) |
| parser.add_argument( |
| "--ci-record", |
| action="store_true", |
| help="Record snapshot for CI and generate PR comment payload" |
| ) |
|
|
| args = parser.parse_args() |
|
|
| |
| if not args.coverage_file.exists(): |
| print(f"Error: Coverage file not found: {args.coverage_file}", file=sys.stderr) |
| sys.exit(1) |
|
|
| with open(args.coverage_file) as f: |
| coverage_data = json.load(f) |
|
|
| |
| if args.ci_record: |
| payload = record_coverage_ci() |
| print(json.dumps(payload, indent=2)) |
| sys.exit(0) |
|
|
| |
| trend_data = get_trend_history(args.output) |
|
|
| |
| snapshot = record_snapshot(coverage_data, args.commit) |
| trend_data = update_trend_data(snapshot, trend_data) |
| write_trend_data(trend_data, args.output) |
|
|
| |
| print("") |
| print("=" * 80) |
| print("COVERAGE SNAPSHOT RECORDED") |
| print("=" * 80) |
| print(f"Timestamp: {snapshot['timestamp']}") |
| print(f"Commit: {snapshot['commit'] or 'unknown'}") |
| print(f"Coverage: {snapshot['overall_coverage']:.2f}%") |
| print(f"Lines: {snapshot['covered_lines']:,} / {snapshot['total_lines']:,}") |
| print(f"Branch: {snapshot['branch_coverage']:.2f}%") |
| print("") |
|
|
| if "delta" in snapshot: |
| delta = snapshot["delta"] |
| symbol = {"increase": "↑", "decrease": "↓", "no_change": "→", "baseline": "="}[delta["direction"]] |
| print(f"Change: {symbol} {delta['absolute_change']:+.2f}% ({delta['relative_change']:+.2f}% relative)") |
|
|
| print("") |
| print("Module Breakdown:") |
| for module, pct in snapshot.get("module_breakdown", {}).items(): |
| print(f" {module}: {pct:.2f}%") |
|
|
| print("") |
|
|
| |
| if args.chart: |
| print(generate_visualization(trend_data)) |
|
|
| |
| if args.regression_check: |
| has_regression, messages = check_regression(trend_data) |
|
|
| print("=" * 80) |
| print("REGRESSION CHECK") |
| print("=" * 80) |
| for msg in messages: |
| print(msg) |
| print("") |
|
|
| if has_regression: |
| print("❌ REGRESSION DETECTED") |
| sys.exit(1) |
| else: |
| print("✅ No regression") |
| sys.exit(0) |
|
|
| |
| if args.forecast: |
| print(forecast_target(trend_data, args.forecast)) |
|
|
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|