Files
troupe-standup/wizard-work/ledger/grade_posts_census.py
T

230 lines
8.3 KiB
Python

#!/usr/bin/env python3
"""Grade the 2026-11-01 per-user post census (Extract lane).
WHAT IT GRADES -- the committed snapshot, not the host and not the calendar.
json the JSON is internally consistent: exactly the four documented
keys, rows carry exactly the six documented fields in order, ids
are 1..N with no gaps, n_users == len(rows), and total_posts ==
sum(post_count).
csv re-rendering the JSON rows as CSV is byte-identical to the
committed 2026-11-01-posts-census.csv (header included).
md re-rendering the JSON rows as the markdown table is byte-identical
to the committed 2026-11-01-posts-census.md, summary line included
(the headline carries the population and the post total, so a stale
count is a fault here, not a footnote).
live only with --live: fetch /users + /posts, derive the rows, and
compare them with the snapshot. An unreachable source is rc 2 --
neither a pass nor a fault.
WHY THE DEFAULT IS OFFLINE
The artifact is dated 2026-11-01 and records one reading of a public API.
A gate that re-fetches on every run grades the network's mood, not the tree:
it cannot run in a clean room and it fails for reasons the artifact did not
choose. The snapshot (JSON) is the record; CSV and MD are its two
independent renderings, and agreeing with each other is the property that
can be graded anywhere. --live re-checks the record against the source when
a reader wants that, and says so.
EXIT CODES
0 all graded legs pass, 1 a leg drifted, 2 cannot grade (missing or
unparseable artifact, or --live with the source unreachable).
USAGE
python3 grade_posts_census.py # offline replay
python3 grade_posts_census.py --live # replay + source re-check
python3 grade_posts_census.py --dir DIR # grade DIR (probe batteries)
python3 grade_posts_census.py --json-out FILE
"""
import argparse
import csv
import io
import json
import os
import sys
import urllib.request
FIELDS = ["id", "name", "username", "email", "city", "post_count"]
BASE = "https://jsonplaceholder.typicode.com"
HERE = os.path.dirname(os.path.abspath(__file__))
STEM = "2026-11-01-posts-census"
def artifact_dir(override=None):
"""Artifacts sit beside the CHECK that grades them.
Not the host path and not the git root: a copy of the directory (a clone,
a tarball, a probe) grades its own bytes. `--dir` points the grader at
another directory for a probe battery.
"""
return os.path.abspath(override) if override else HERE
def render_csv(rows):
buf = io.StringIO()
w = csv.writer(buf, lineterminator="\n")
w.writerow(FIELDS)
for r in rows:
w.writerow([r[f] for f in FIELDS])
return buf.getvalue()
def render_md(rows):
total = sum(int(r["post_count"]) for r in rows)
out = ["# Per-user Post Census (JSONPlaceholder)", "",
"Table of each user with their post count "
f"({len(rows)} users, {total} posts total).", "",
"| id | name | username | email | city | post_count |",
"|---:|---|---:|---:|---:|---:|"]
for r in rows:
out.append(f"| {r['id']} | {r['name']} | {r['username']} | "
f"{r['email']} | {r['city']} | {r['post_count']} |")
return "\n".join(out) + "\n"
def load_json(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
def check_json_shape(j):
problems = []
if list(j.keys()) != ["source", "n_users", "total_posts", "rows"]:
problems.append(f"top-level keys {list(j.keys())} != "
"['source', 'n_users', 'total_posts', 'rows']")
rows = j.get("rows")
if not isinstance(rows, list) or not rows:
return ["rows is not a non-empty list"]
for i, r in enumerate(rows):
if list(r.keys()) != FIELDS:
problems.append(f"row {i} keys {list(r.keys())} != {FIELDS}")
elif not isinstance(r["id"], int) or not isinstance(r["post_count"], int):
problems.append(f"row {i} id/post_count are not integers")
ids = [r.get("id") for r in rows]
if ids != list(range(1, len(rows) + 1)):
problems.append(f"ids {ids[:3]}... are not 1..{len(rows)}")
if j.get("n_users") != len(rows):
problems.append(f"n_users {j.get('n_users')} != {len(rows)}")
total = sum(int(r["post_count"]) for r in rows if isinstance(r, dict))
if j.get("total_posts") != total:
problems.append(f"total_posts {j.get('total_posts')} != {total}")
return problems
def fetch(path):
with urllib.request.urlopen(BASE + path, timeout=20) as r:
return json.loads(r.read().decode("utf-8"))
def live_rows():
users = fetch("/users")
posts = fetch("/posts")
by_id = {u["id"]: u for u in users}
counts = {}
for p in posts:
counts[p["userId"]] = counts.get(p["userId"], 0) + 1
rows = []
for uid in sorted(by_id):
u = by_id[uid]
rows.append({"id": uid, "name": u["name"], "username": u["username"],
"email": u["email"], "city": u["address"]["city"],
"post_count": counts.get(uid, 0)})
return rows
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--live", action="store_true",
help="also re-derive from JSONPlaceholder")
ap.add_argument("--json-out", default=None)
ap.add_argument("--dir", default=None,
help="grade artifacts in this directory instead of beside "
"the script (probe batteries)")
args = ap.parse_args()
outdir = artifact_dir(args.dir)
jpath = os.path.join(outdir, STEM + ".json")
cpath = os.path.join(outdir, STEM + ".csv")
mpath = os.path.join(outdir, STEM + ".md")
missing = [p for p in (jpath, cpath, mpath) if not os.path.isfile(p)]
if missing:
print("CANNOT GRADE missing artifact(s):")
for p in missing:
print(" -", p)
return 2
try:
snap = load_json(jpath)
except Exception as e:
print(f"CANNOT GRADE json unreadable: {e}")
return 2
results = []
legs = {"json": 0, "csv": 0, "md": 0}
shape = check_json_shape(snap)
if shape:
legs["json"] = 1
results.append(("json", shape))
rows = snap["rows"]
with open(cpath, encoding="utf-8", newline="") as f:
got_csv = f.read()
exp_csv = render_csv(rows)
if got_csv != exp_csv:
legs["csv"] = 1
results.append(("csv", [f"csv {len(got_csv)} B != re-rendered "
f"{len(exp_csv)} B"]))
with open(mpath, encoding="utf-8") as f:
got_md = f.read()
exp_md = render_md(rows)
if got_md != exp_md:
legs["md"] = 1
results.append(("md", [f"md {len(got_md)} B != re-rendered "
f"{len(exp_md)} B"]))
live_note = ""
if args.live:
try:
derived = live_rows()
except Exception as e:
print(f"CANNOT GRADE data source unreachable: {e}")
return 2
if derived != rows:
legs["live"] = 1
diff = [i for i in range(max(len(derived), len(rows)))
if i >= len(rows) or i >= len(derived)
or derived[i] != rows[i]]
results.append(("live", [f"{len(diff)} row(s) differ from the "
f"live API, first at index {diff[0]}"]))
else:
live_note = f", live {len(derived)} rows match the API"
for leg in ("json", "csv", "md", ("live" if args.live else None)):
if leg is None:
continue
state = "PASS" if legs.get(leg, 0) == 0 else "FAIL"
print(f"{state} {leg}")
bad = [name for name, _ in results]
if bad:
print("POSTS CENSUS MISMATCH")
for name, probs in results:
for p in probs:
print(f" - {name}: {p}")
return 1
total = sum(int(r["post_count"]) for r in rows)
print(f"VERDICT=PASS posts-census-2026-11-01 users={len(rows)} "
f"posts={total} rows={len(rows)}{live_note}")
if args.json_out:
with open(args.json_out, "w", encoding="utf-8") as f:
json.dump({"verdict": "PASS", "users": len(rows),
"posts": total, "live": bool(args.live)}, f, indent=2)
return 0
if __name__ == "__main__":
sys.exit(main())