Skip to content

Commit 4130783

Browse files
committed
feat(verify): add 'status' command — aggregate verification state to JSON
Emits data/_verify/status.json: per-category verified counts + Tier 0 bands + promotion candidates, as the synced source of truth for how much is verified. Refs #1
1 parent df12ce1 commit 4130783

1 file changed

Lines changed: 72 additions & 0 deletions

File tree

app/verify/cli.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,20 @@
1212
from __future__ import annotations
1313

1414
import argparse
15+
import json
1516
import subprocess
1617
from collections import Counter, defaultdict
1718
from datetime import datetime, timezone
19+
from pathlib import Path
1820

1921
from . import crossref, http_check, ledger, offline, promote
2022
from .common import (
2123
CATEGORIES,
2224
SCORES_PATH,
25+
VERIFY_DIR,
2326
Record,
2427
configure_stdout,
28+
ensure_verify_dirs,
2529
foreign_key_sets,
2630
load_all,
2731
repo_path,
@@ -230,6 +234,68 @@ def _print_markdown(hist, scored, hard_flags) -> None:
230234
print(f"| {n} | `{name}` |")
231235

232236

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+
233299
def cmd_report(args: argparse.Namespace) -> int:
234300
if not SCORES_PATH.exists():
235301
print("no scores cache — run `python -m app.verify score` first")
@@ -583,6 +649,12 @@ def build_parser() -> argparse.ArgumentParser:
583649
rp = sub.add_parser("report", help="summarize latest ledger state")
584650
rp.set_defaults(func=cmd_report)
585651

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+
586658
cu = sub.add_parser("check-urls", help="Tier 1: source_urls HTTP liveness")
587659
cu.add_argument("--category", nargs="*", choices=CATEGORIES, help="limit to categories")
588660
cu.add_argument("--max", type=int, default=500, help="number of frontier records to target")

0 commit comments

Comments
 (0)