playbook/antigravity-awesome-skills/tools/scripts/generate_registry_report.py

195 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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())