#!/usr/bin/env python3 """ Registry Report Generator — Antigravity Awesome Skills Generates a consolidated health report for the skill registry. Combines scoring, security scanning, and drift detection into a single data/registry-report.json file suitable for dashboards and CI monitoring. Usage: node tools/scripts/run-python.js tools/scripts/generate_registry_report.py node tools/scripts/run-python.js tools/scripts/generate_registry_report.py --output custom/path.json """ from __future__ import annotations import argparse import json import sys from datetime import datetime, timezone from pathlib import Path from typing import Any from _project_paths import find_repo_root from validate_skills import configure_utf8_output from score_skills import score_all_skills, build_summary, SkillScore from detect_drift import ( load_baseline, build_current_entries, compute_drift, BASELINE_FILE, ) # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- DEFAULT_OUTPUT = Path("data") / "registry-report.json" REPORT_SCHEMA_VERSION = 1 # --------------------------------------------------------------------------- # Report assembly # --------------------------------------------------------------------------- def _risk_breakdown_sorted(summary: dict) -> list[dict]: return [ {"risk": k, "count": v} for k, v in sorted( summary.get("risk_breakdown", {}).items(), key=lambda kv: -kv[1], ) ] def _score_distribution_list(summary: dict) -> list[dict]: order = ["excellent", "good", "needs_improvement", "critical"] dist = summary.get("score_distribution", {}) return [{"label": label, "count": dist.get(label, 0)} for label in order] def build_report( scores: list[SkillScore], version: str, drift_summary: dict | None = None, ) -> dict[str, Any]: summary = build_summary(scores) skills_payload = sorted( [s.to_dict() for s in scores], key=lambda s: s["scores"]["total"], ) report: dict[str, Any] = { "schema_version": REPORT_SCHEMA_VERSION, "generated_at": datetime.now(timezone.utc).isoformat(), "skills_version": version, "summary": { "total_skills": summary.get("total_skills", 0), "average_score": summary.get("average_score", 0.0), "min_score": summary.get("min_score", 0.0), "max_score": summary.get("max_score", 0.0), "score_distribution": _score_distribution_list(summary), "risk_breakdown": _risk_breakdown_sorted(summary), "security": { "flag_errors": summary.get("flag_errors", 0), "flag_warnings": summary.get("flag_warnings", 0), }, }, "skills": skills_payload, } if drift_summary is not None: report["drift"] = drift_summary return report # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def _print_summary_banner(report: dict) -> None: configure_utf8_output() s = report["summary"] dist = {d["label"]: d["count"] for d in s["score_distribution"]} sec = s["security"] print(f"\n{'═' * 60}") print("📋 REGISTRY REPORT GENERATED") print(f"{'─' * 60}") print(f" Version : {report['skills_version']}") print(f" Skills : {s['total_skills']}") print(f" Avg score : {s['average_score']:.1f}") print(f" ✅ Excellent : {dist.get('excellent', 0)}") print(f" đŸŸĸ Good : {dist.get('good', 0)}") print(f" âš ī¸ Needs work : {dist.get('needs_improvement', 0)}") print(f" ❌ Critical : {dist.get('critical', 0)}") print(f" Security errors: {sec['flag_errors']}") print(f" Security warns : {sec['flag_warnings']}") if "drift" in report: d = report["drift"] print(f" Drift detected : {'yes' if d.get('has_drift') else 'no'}") if d.get("has_drift"): print(f" Added : {len(d.get('added', []))}") print(f" Removed : {len(d.get('removed', []))}") print(f" Modified : {len(d.get('drifted', []))}") print(f"{'═' * 60}\n") def main(argv: list[str] | None = None) -> int: configure_utf8_output() parser = argparse.ArgumentParser( description="Generate a consolidated Antigravity skill registry health report." ) parser.add_argument( "--output", metavar="FILE", default=str(DEFAULT_OUTPUT), help=f"Output path for JSON report (default: {DEFAULT_OUTPUT}).", ) parser.add_argument( "--no-drift", action="store_true", help="Skip drift detection (faster, useful when no baseline exists).", ) args = parser.parse_args(argv) repo_root = find_repo_root(__file__) skills_dir = repo_root / "skills" output_path = repo_root / args.output # Read version from package.json version = "unknown" pkg_path = repo_root / "package.json" if pkg_path.exists(): try: version = json.loads(pkg_path.read_text(encoding="utf-8")).get("version", "unknown") except Exception: pass print(f"📐 Scoring {skills_dir} ...") scores = score_all_skills(skills_dir) print(f" {len(scores)} skills scored.") drift_summary: dict | None = None if not args.no_drift: baseline_path = repo_root / BASELINE_FILE if baseline_path.exists(): print("🔍 Computing drift ...") baseline = load_baseline(baseline_path) current = build_current_entries(skills_dir) drift_report = compute_drift(baseline, current) drift_summary = drift_report.to_dict() else: print("â„šī¸ No drift baseline found — skipping drift check.") print(" Run: npm run drift:update to create one.") report = build_report(scores, version, drift_summary) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text( json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8", ) print(f"💾 Report saved → {output_path}") _print_summary_banner(report) return 0 if __name__ == "__main__": sys.exit(main())