104 lines
4.0 KiB
Python
104 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Re-verification test for the ledger repo-extraction artifact.
|
|
|
|
Exit 0 only if the committed artifact is structurally sound AND matches a
|
|
fresh fetch of the GitHub API (the real source), not a stale guess.
|
|
"""
|
|
import csv
|
|
import json
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
|
|
REPO_DIR = sys.argv[1] if len(sys.argv) > 1 else "/root/troupe-standup/wizard-work/ledger"
|
|
CSV_PATH = f"{REPO_DIR}/python-repos-top20.csv"
|
|
JSON_PATH = f"{REPO_DIR}/python-repos-top20.json"
|
|
|
|
def fetch(url):
|
|
req = urllib.request.Request(url, headers={"User-Agent": "ledger-wizard"})
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
return json.loads(r.read().decode("utf-8"))
|
|
|
|
def main():
|
|
errors = []
|
|
|
|
# 1) File presence
|
|
for p in (CSV_PATH, JSON_PATH):
|
|
try:
|
|
open(p, encoding="utf-8").close()
|
|
except FileNotFoundError:
|
|
errors.append(f"missing {p}")
|
|
|
|
if errors:
|
|
print("FAIL: " + "; ".join(errors))
|
|
return 1
|
|
|
|
# 2) JSON shape
|
|
with open(JSON_PATH, encoding="utf-8") as f:
|
|
payload = json.load(f)
|
|
rows = payload.get("rows")
|
|
if not isinstance(rows, list) or len(rows) != 20:
|
|
errors.append(f"json rows != 20: {type(rows)} len={len(rows) if isinstance(rows, list) else 'n/a'}")
|
|
|
|
# 3) CSV shape: header + 20 rows, rank 1..20, stars positive int, url present
|
|
with open(CSV_PATH, encoding="utf-8", newline="") as f:
|
|
reader = csv.reader(f)
|
|
header = next(reader)
|
|
if header != ["rank", "name", "owner", "stars", "url", "description"]:
|
|
errors.append(f"csv header {header}")
|
|
csv_rows = list(reader)
|
|
if len(csv_rows) != 20:
|
|
errors.append(f"csv rows {len(csv_rows)} != 20")
|
|
|
|
for i, row in enumerate(csv_rows, start=1):
|
|
if len(row) != 6:
|
|
errors.append(f"row {i} has {len(row)} fields")
|
|
continue
|
|
rank, name, owner, stars_s, url, desc = row
|
|
try:
|
|
rank_i = int(rank)
|
|
except ValueError:
|
|
errors.append(f"row {i} rank not int: {rank!r}")
|
|
rank_i = None
|
|
if rank_i != i:
|
|
errors.append(f"row {i} rank {rank_i} != sequence {i}")
|
|
if not stars_s.isdigit() or int(stars_s) <= 0:
|
|
errors.append(f"row {i} stars not positive int: {stars_s!r}")
|
|
if not url.startswith("https://github.com/"):
|
|
errors.append(f"row {i} url malformed: {url!r}")
|
|
|
|
# 4) Cross-check vs fresh API fetch (the actual evidence source).
|
|
# Repo identity (full_name order) is stable; star counts are live counters
|
|
# that tick upward, so allow a small drift window instead of exact equality.
|
|
try:
|
|
q = urllib.parse.quote("language:python")
|
|
url = f"https://api.github.com/search/repositories?q={q}&sort=stars&order=desc&per_page=20"
|
|
data = fetch(url)
|
|
if data.get("incomplete_results"):
|
|
errors.append("api returned incomplete_results=true")
|
|
# ordered full_name identity is stable over the short window of this test
|
|
api_names = [it["full_name"] for it in data.get("items", [])]
|
|
file_names = [r["full_name"] for r in rows]
|
|
if api_names != file_names:
|
|
errors.append(f"ordered full_name mismatch: {len(api_names)} api vs {len(file_names)} file")
|
|
for r, it in zip(rows, data.get("items", [])):
|
|
drift = abs(r["stars"] - it["stargazers_count"])
|
|
if drift > 5:
|
|
errors.append(f"star drift for {r['full_name']}: file={r['stars']} api={it['stargazers_count']} drift={drift}")
|
|
except Exception as exc: # noqa: BLE001
|
|
errors.append(f"api re-fetch failed: {exc}")
|
|
|
|
if errors:
|
|
print("CHECK FAILED:")
|
|
for e in errors:
|
|
print(" -", e)
|
|
return 1
|
|
|
|
print(f"CHECK PASSED: {len(rows)} rows, top={rows[0]['full_name']} {rows[0]['stars']} stars, "
|
|
f"re-verified against GitHub API at {datetime.now(timezone.utc).isoformat()}")
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|