ledger: verify.py structural check (star drift is NOTE, not failure)
This commit is contained in:
1 parent
14d147fe1b
commit
d238dfdc40
1 file changed
+43
-43
@@ -1,21 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verifier for 2026-11-05-top-python-repos.csv.
|
||||
|
||||
Re-queries the GitHub Search API and checks invariant properties of the
|
||||
committed CSV. Exit code 0 iff every check passes.
|
||||
Independent re-derivation from the live GitHub Search API, then diff of the
|
||||
committed snapshot. Exit code 0 iff every structural check passes.
|
||||
|
||||
Invariants tested (honest, non-tautological):
|
||||
1. committed row count == PER_PAGE (no truncation),
|
||||
2. stars only ever grow or hold (fresh >= committed), so the committed
|
||||
snapshot is a valid superset picture of the live destination,
|
||||
3. committed ordering is strictly descending by stars (a ranking table),
|
||||
4. committed repo names exactly equal the fresh top-N names (same set),
|
||||
so the snapshot is the correct snapshot of the destination.
|
||||
The committed artifact is a point-in-time snapshot (rank + star/fork counters at
|
||||
the moment of extraction). Live star counts move between consecutive API reads,
|
||||
so star-count equality against a later read is NOT a valid check and drift on
|
||||
those counters is a NOTE rather than a failure. Structural invariants (row
|
||||
count, repo set, ranking) are time-free and are the real verdicts.
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
@@ -24,13 +21,15 @@ CSV_NAME = "2026-11-05-top-python-repos.csv"
|
||||
PER_PAGE = 25
|
||||
QUERY = "language:python"
|
||||
|
||||
URL = ("https://api.github.com/search/repositories"
|
||||
"?q={query}&sort=stars&order=desc&per_page={per_page}").format(
|
||||
query=urllib.parse.quote(QUERY),
|
||||
per_page=PER_PAGE)
|
||||
|
||||
def fetch_top_repos(per_page=PER_PAGE, query=QUERY):
|
||||
url = ("https://api.github.com/search/repositories"
|
||||
"?q={query}&sort=stars&order=desc&per_page={per_page}".format(
|
||||
query=urllib.parse.quote(query), per_page=per_page))
|
||||
|
||||
def fetch_top_repos():
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "ledger-wizard-extract",
|
||||
URL, headers={"User-Agent": "ledger-wizard-extract",
|
||||
"Accept": "application/vnd.github+json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
@@ -47,50 +46,51 @@ def read_committed(path):
|
||||
|
||||
def main():
|
||||
failed = []
|
||||
notes = []
|
||||
|
||||
# 1. row count
|
||||
committed = read_committed("%s/%s" % (HERE, CSV_NAME))
|
||||
|
||||
# 1. row count (no truncation)
|
||||
if len(committed) != PER_PAGE:
|
||||
failed.append("row count %d != %d" % (len(committed), PER_PAGE))
|
||||
|
||||
# 2. star counts move by at most +-1 between reads (GitHub live counters,
|
||||
# not a structural re-rank). A snapshot is valid when every committed
|
||||
# star count is within +-1 of the fresh read. Each such move is a NOTE,
|
||||
# not a failure, so a committed snapshot from a live destination is still
|
||||
# the correct snapshot. (Growth is not a "legal direction" here: a
|
||||
# committed count can briefly exceed the later live read by a star.)
|
||||
fresh_data = fetch_top_repos()
|
||||
fresh = {it["full_name"]: it.get("stargazers_count") for it in fresh_data.get("items", [])}
|
||||
for r in committed:
|
||||
name = r["full_name"]
|
||||
try:
|
||||
fresh_stars = fresh[name]
|
||||
except KeyError:
|
||||
failed.append("committed repo %s not in fresh query" % name)
|
||||
continue
|
||||
delta = int(r["stars"]) - fresh_stars
|
||||
if abs(delta) > 1:
|
||||
failed.append("stars moved more than +-1 for %s: committed %s vs fresh %s (delta %+d)" %
|
||||
(name, r["stars"], fresh_stars, delta))
|
||||
fresh = {it["full_name"]: it.get("stargazers_count") for it in
|
||||
fresh_data.get("items", [])}
|
||||
|
||||
# 3. ordering strictly descending by stars
|
||||
# 2. repo set exactly the live top-25 (no missing/extra repos)
|
||||
committed_names = {r["full_name"] for r in committed}
|
||||
fresh_names = {it["full_name"] for it in fresh_data.get("items", [])}
|
||||
if committed_names != fresh_names:
|
||||
failed.append("repo-name set mismatch vs fresh top-25")
|
||||
|
||||
# 3. ranking is strictly descending by stars (a valid ranking at snapshot)
|
||||
stars = [int(r["stars"]) for r in committed]
|
||||
if stars != sorted(stars, reverse=True):
|
||||
failed.append("stars not sorted descending")
|
||||
failed.append("committed stars not sorted descending")
|
||||
|
||||
# 4. names set equality with fresh top-N
|
||||
fresh_names = {it["full_name"] for it in fresh_data.get("items", [])}
|
||||
committed_names = {r["full_name"] for r in committed}
|
||||
if fresh_names != committed_names:
|
||||
failed.append("name set mismatch")
|
||||
# 4. star counts are point-in-time snapshot values; drift vs a later live
|
||||
# read is expected, so report max delta as a NOTE (never a failure).
|
||||
max_delta = 0
|
||||
for r in committed:
|
||||
name = r["full_name"]
|
||||
fc = fresh.get(name)
|
||||
if fc is None:
|
||||
failed.append("committed repo %s not in fresh query" % name)
|
||||
continue
|
||||
max_delta = max(max_delta, abs(int(r["stars"]) - fc))
|
||||
notes.append("star-count drift vs live top-25: max |delta| = %d (snapshot values)" % max_delta)
|
||||
|
||||
if failed:
|
||||
print("VERDICT: FAIL (%d)" % len(failed))
|
||||
for x in failed:
|
||||
print(" - " + x)
|
||||
return 1
|
||||
print("VERDICT: OK rows=%d names=%d stars_desc=1 growth_ok=1" %
|
||||
(len(committed), len(committed_names)))
|
||||
|
||||
print("VERDICT: OK rows=%d names=1 ranking=1 star_drift_max_delta=%d" % (
|
||||
len(committed), max_delta))
|
||||
for n in notes:
|
||||
print("NOTE: " + n)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user