#!/usr/bin/env python3 """score-sitting.py — the blind sitting's tally, derived rather than asserted. The kit publishes two files and no summary number between them: the sealed key (`blind-key.json`) and the listener's answers exactly as typed (`listener-answers-raw-2026-09-06.txt`, timestamped before the key was opened). This joins them, and every count the page prints about the sitting comes out of this join. python3 score-sitting.py # print the tally python3 score-sitting.py --json # the same thing as JSON It reads the answers' own FINAL line — the tally the listener's sheet closes with, naming which trials went to A and which to B — rather than reading his prose, because deciding what "B is way better" means is a judgement and this script does arithmetic. The prose is beside it in the same file for anyone who wants to check the reading. Standard library only, no arguments needed, and it prints the digests of both inputs so a run of it is a receipt rather than an assertion. """ import argparse import hashlib import json import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) KEY = os.path.join(HERE, "blind-key.json") ANSWERS = os.path.join(HERE, "listener-answers-raw-2026-09-06.txt") RECORDS = os.path.join(HERE, "companion", "records.json") #: A request "asks for house" when its registered style names house. The four #: are deep house, tech house, disco house and progressive house; the styles #: come from the render records, never from a list typed here. HOUSE = "house" FINAL = re.compile(r"^FINAL:.*?A on ([0-9,\s]+?)\s*\(\d+\);\s*B on ([0-9,\s]+?)\s*\(\d+\)", re.M) def digest(path): return hashlib.sha256(open(path, "rb").read()).hexdigest() def directional(answers_text): """{trial: 'A'|'B'} from the answers' own closing tally.""" match = FINAL.search(answers_text) if not match: raise SystemExit("score-sitting: no FINAL line in the answers file") picks = {} for side, group in (("A", match.group(1)), ("B", match.group(2))): for trial in group.split(","): picks[int(trial.strip())] = side return picks def score(): key = json.load(open(KEY, encoding="utf-8")) answers = open(ANSWERS, encoding="utf-8").read() picks = directional(answers) out = { "inputs": { "blind-key.json": digest(KEY), "listener-answers-raw-2026-09-06.txt": digest(ANSWERS), "companion/records.json": digest(RECORDS), }, "presentations": len(key["key"]), "scoring": {"trials": 0, "directional": 0, "no_preference": 0, "by_arm": {}}, "sentinels": {"trials": 0, "heard_identical": 0, "directional": 0, "directional_trials": [], "all_pairs_identical_bytes": True}, "adapter_wins": [], "base_wins": [], "by_style": {}, "house_asking_requests": {"styles": [], "trials": 0, "directional": 0, "no_preference": 0, "by_arm": {}}, } styles = {clip["prompt_id"]: clip["style"] for clip in json.load(open(RECORDS, encoding="utf-8"))["clips"]} for trial in key["key"]: n = trial["trial"] side = picks.get(n) if trial["kind"] == "sentinel": out["sentinels"]["trials"] += 1 if trial["A"]["sha256"] != trial["B"]["sha256"]: out["sentinels"]["all_pairs_identical_bytes"] = False if side is None: out["sentinels"]["heard_identical"] += 1 else: out["sentinels"]["directional"] += 1 out["sentinels"]["directional_trials"].append(n) continue out["scoring"]["trials"] += 1 style = styles[trial["prompt_id"]] asks_for_house = HOUSE in style.lower() house = out["house_asking_requests"] if asks_for_house: house["trials"] += 1 if style not in house["styles"]: house["styles"].append(style) row = out["by_style"].setdefault( style, {"trials": 0, "directional": 0, "no_preference": 0, "by_arm": {}}) row["trials"] += 1 if side is None: out["scoring"]["no_preference"] += 1 row["no_preference"] += 1 if asks_for_house: house["no_preference"] += 1 continue out["scoring"]["directional"] += 1 row["directional"] += 1 arm = trial[side]["arm"] out["scoring"]["by_arm"][arm] = out["scoring"]["by_arm"].get(arm, 0) + 1 row["by_arm"][arm] = row["by_arm"].get(arm, 0) + 1 if asks_for_house: house["directional"] += 1 house["by_arm"][arm] = house["by_arm"].get(arm, 0) + 1 (out["adapter_wins"] if arm != "base" else out["base_wins"]).append(n) out["adapter_win_styles"] = { n: styles[t["prompt_id"]] for t in key["key"] for n in [t["trial"]] if n in out["adapter_wins"]} return out def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--json", action="store_true", help="emit JSON instead of prose") args = ap.parse_args() out = score() if args.json: json.dump(out, sys.stdout, indent=1) print() return 0 s, t = out["scoring"], out["sentinels"] print("blind-key.json sha256 %s" % out["inputs"]["blind-key.json"]) print("listener-answers-raw-2026-09-06.txt sha256 %s" % out["inputs"]["listener-answers-raw-2026-09-06.txt"]) print() print("presentations ................ %d" % out["presentations"]) print("scoring trials ............... %d" % s["trials"]) print(" a preference expressed ..... %d" % s["directional"]) print(" no difference heard ........ %d" % s["no_preference"]) for arm in sorted(s["by_arm"]): print(" preferred %-18s %d" % (arm, s["by_arm"][arm])) print(" the adapter's wins ......... %s" % ", ".join(str(n) for n in out["adapter_wins"])) house = out["house_asking_requests"] print("the four requests that ask for house (%s)" % ", ".join(house["styles"])) print(" a preference expressed ..... %d" % house["directional"]) print(" no difference heard ........ %d" % house["no_preference"]) for arm in sorted(house["by_arm"]): print(" preferred %-18s %d" % (arm, house["by_arm"][arm])) print("the adapter's four wins, by the request's registered style:") for n in out["adapter_wins"]: print(" trial %-3d %s" % (n, out["adapter_win_styles"][n])) print("sentinels .................... %d (identical file both sides: %s)" % (t["trials"], t["all_pairs_identical_bytes"])) print(" heard as identical ......... %d" % t["heard_identical"]) print(" a difference reported ...... %d %s" % (t["directional"], t["directional_trials"] or "")) return 0 if __name__ == "__main__": sys.exit(main())