|
12 | 12 | from __future__ import annotations |
13 | 13 |
|
14 | 14 | import argparse |
| 15 | +import json |
15 | 16 | import subprocess |
16 | 17 | from collections import Counter, defaultdict |
17 | 18 | from datetime import datetime, timezone |
| 19 | +from pathlib import Path |
18 | 20 |
|
19 | 21 | from . import crossref, http_check, ledger, offline, promote |
20 | 22 | from .common import ( |
21 | 23 | CATEGORIES, |
22 | 24 | SCORES_PATH, |
| 25 | + VERIFY_DIR, |
23 | 26 | Record, |
24 | 27 | configure_stdout, |
| 28 | + ensure_verify_dirs, |
25 | 29 | foreign_key_sets, |
26 | 30 | load_all, |
27 | 31 | repo_path, |
@@ -230,6 +234,68 @@ def _print_markdown(hist, scored, hard_flags) -> None: |
230 | 234 | print(f"| {n} | `{name}` |") |
231 | 235 |
|
232 | 236 |
|
| 237 | +def cmd_status(args: argparse.Namespace) -> int: |
| 238 | + """Aggregate the verification state into one JSON file (the synced source of |
| 239 | + truth for "how much is verified"): per-category `verified` counts + Tier 0 |
| 240 | + bands + promotion candidates. Default output: data/_verify/status.json.""" |
| 241 | + records = load_all() |
| 242 | + _, _, soc_release = foreign_key_sets(records) |
| 243 | + now_year = offline.now_year_today() |
| 244 | + |
| 245 | + by_category: dict[str, dict] = {} |
| 246 | + tot = ver = g = y = r = 0 |
| 247 | + for cat in CATEGORIES: |
| 248 | + ct = cv = cg = cy = cr = 0 |
| 249 | + for rec in records[cat]: |
| 250 | + if not rec.slug: |
| 251 | + continue |
| 252 | + ct += 1 |
| 253 | + if rec.verified: |
| 254 | + cv += 1 |
| 255 | + band = offline.score_record(rec, now_year, soc_release).band |
| 256 | + cg += band == "green" |
| 257 | + cy += band == "yellow" |
| 258 | + cr += band == "red" |
| 259 | + by_category[cat] = { |
| 260 | + "total": ct, |
| 261 | + "verified": cv, |
| 262 | + "verified_pct": round(100 * cv / ct, 2) if ct else 0.0, |
| 263 | + "green": cg, |
| 264 | + "yellow": cy, |
| 265 | + "red": cr, |
| 266 | + # green = high-confidence band; the promotion candidate pool. |
| 267 | + "promotable": cg, |
| 268 | + } |
| 269 | + tot += ct; ver += cv; g += cg; y += cy; r += cr |
| 270 | + |
| 271 | + status = { |
| 272 | + "generated_at": _now_iso(), |
| 273 | + "schema": 1, |
| 274 | + "totals": { |
| 275 | + "records": tot, |
| 276 | + "verified": ver, |
| 277 | + "verified_pct": round(100 * ver / tot, 2) if tot else 0.0, |
| 278 | + "green": g, |
| 279 | + "yellow": y, |
| 280 | + "red": r, |
| 281 | + "promotable": g, |
| 282 | + }, |
| 283 | + "by_category": by_category, |
| 284 | + } |
| 285 | + blob = json.dumps(status, indent=2, ensure_ascii=False) + "\n" |
| 286 | + |
| 287 | + if args.stdout: |
| 288 | + print(blob, end="") |
| 289 | + else: |
| 290 | + out = args.output or (VERIFY_DIR / "status.json") |
| 291 | + ensure_verify_dirs() |
| 292 | + out.write_text(blob, encoding="utf-8") |
| 293 | + print(f"wrote verification status: {out} " |
| 294 | + f"({ver}/{tot} verified = {100*ver/tot:.2f}%, " |
| 295 | + f"{g} green / {y} yellow / {r} red)") |
| 296 | + return 0 |
| 297 | + |
| 298 | + |
233 | 299 | def cmd_report(args: argparse.Namespace) -> int: |
234 | 300 | if not SCORES_PATH.exists(): |
235 | 301 | print("no scores cache — run `python -m app.verify score` first") |
@@ -583,6 +649,12 @@ def build_parser() -> argparse.ArgumentParser: |
583 | 649 | rp = sub.add_parser("report", help="summarize latest ledger state") |
584 | 650 | rp.set_defaults(func=cmd_report) |
585 | 651 |
|
| 652 | + st = sub.add_parser("status", help="write the aggregated verification status JSON") |
| 653 | + st.add_argument("--output", type=Path, default=None, |
| 654 | + help="output path (default: data/_verify/status.json)") |
| 655 | + st.add_argument("--stdout", action="store_true", help="print JSON instead of writing a file") |
| 656 | + st.set_defaults(func=cmd_status) |
| 657 | + |
586 | 658 | cu = sub.add_parser("check-urls", help="Tier 1: source_urls HTTP liveness") |
587 | 659 | cu.add_argument("--category", nargs="*", choices=CATEGORIES, help="limit to categories") |
588 | 660 | cu.add_argument("--max", type=int, default=500, help="number of frontier records to target") |
|
0 commit comments