iftypeset/tools/coverage_summary.py
codex e92f1c3b93
Some checks are pending
ci / ci (push) Waiting to run
iftypeset: document CI pipeline + Playwright + font contract
2026-01-08 18:10:41 +00:00

93 lines
3 KiB
Python

#!/usr/bin/env python3
"""
Generate a deterministic summary of spec/coverage/*.json (sections + rule ids).
Outputs JSON and a short Markdown summary for status docs.
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
def load_coverage_files(coverage_dir: Path) -> list[dict]:
files = sorted(coverage_dir.glob("*.json"))
payloads = []
for path in files:
payload = json.loads(path.read_text())
payloads.append({"path": path, "data": payload})
return payloads
def summarize(payloads: list[dict]) -> dict:
books = {}
all_rule_ids: set[str] = set()
for item in payloads:
data = item["data"]
book = data.get("book", "UNKNOWN")
sections = data.get("sections", [])
status_counts: dict[str, int] = {}
rule_ids: set[str] = set()
for section in sections:
status = section.get("status", "unknown")
status_counts[status] = status_counts.get(status, 0) + 1
for rid in section.get("rule_ids", []):
rule_ids.add(rid)
books[book] = {
"sections": len(sections),
"rules": len(rule_ids),
"status_counts": dict(sorted(status_counts.items())),
}
all_rule_ids.update(rule_ids)
return {
"generated_utc": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
"books": dict(sorted(books.items())),
"total_rules": len(all_rule_ids),
}
def write_json(out_path: Path, summary: dict) -> None:
out_path.write_text(json.dumps(summary, indent=2) + "\n")
def write_md(out_path: Path, summary: dict) -> None:
lines: list[str] = []
lines.append("# Coverage Summary")
lines.append("")
lines.append(f"Generated (UTC): {summary['generated_utc']}")
lines.append("")
lines.append("## By Book")
lines.append("")
for book, stats in summary["books"].items():
lines.append(f"- {book}: sections={stats['sections']}, rules={stats['rules']}, status_counts={stats['status_counts']}")
lines.append("")
lines.append(f"Total unique rule_ids: {summary['total_rules']}")
lines.append("")
out_path.write_text("\n".join(lines))
def main() -> int:
parser = argparse.ArgumentParser(description="Summarize spec coverage files.")
parser.add_argument("--coverage-dir", default="spec/coverage", help="Coverage directory (default: spec/coverage)")
parser.add_argument("--out-json", default="out/coverage-summary.json", help="Output JSON path")
parser.add_argument("--out-md", default="out/coverage-summary.md", help="Output Markdown path")
args = parser.parse_args()
coverage_dir = Path(args.coverage_dir)
payloads = load_coverage_files(coverage_dir)
summary = summarize(payloads)
write_json(Path(args.out_json), summary)
write_md(Path(args.out_md), summary)
return 0
if __name__ == "__main__":
raise SystemExit(main())