#!/usr/bin/env python3 # A documented one-shot, not part of the OCaml build (spec's "no hand-written # JSON parser" rule is about colitur's OWN sexp format -- reading someone # else's JSON needs a reader, same reasoning as tools/bootstrap_sanctoral.ml's # own header comment on its INI reader). Task 16: extracts the missalemeum # oracle fixture (test/fixtures/missalemeum-ef-2026-2027.txt) that # test/test_oracle.ml compares colitur against -- there is no JSON library in # this project's frozen deps, so the fixture is shaped here, once, outside # the test, exactly as test/test_differential.ml's lectio fixture already is # (see that file's own header comment). # # Usage: # python3 tools/extract_missalemeum_oracle.py # # is ~/git/projects/lectio/sources/ already unpacked (`tar xzf # snapshot.tar.gz` in that directory -- lectio's own scripts/snapshot- # sources.sh does this), so /missalemeum/en/YYYY-MM-DD.json # exists for every day. # # One line per day, pipe-separated: # date|rank|colors|title|tempora|commemorations|displaced|n_masses|commemoration_ids # # - rank/colors/title/tempora/commemorations/displaced are info.rank, # info.colors (sorted, concatenated, e.g. "pv"), info.title, info.tempora # ("-" for JSON null), the ";"-joined titles of info.commemorations and # info.displaced ("-" for an empty list), verbatim from the JSON's own # English strings -- not translated or slugified, so the comparator (and a # human auditor) can match colitur's own celebration names against them. # - n_masses is len(the day's JSON array). Four days (Christmas, All Souls) # carry more than one Mass; entry[0]'s info block is used throughout (rank/ # colors/tempora/commemorations/displaced are IDENTICAL across every Mass # of the same day for all 730 days -- checked below, not assumed; see the # task report for why entry[0] loses nothing). # - commemoration_ids (Task B, branch ef-rg16a) is the ";"-joined info. # commemorations[*].id, index-aligned with the commemorations field ("-" # for an empty list, same convention) -- e.g. "sancti:01-05:4:r". Added so # test_oracle.ml's identity comparison has a second, independent signal # beyond the title text (the id encodes the commemorated saint's own rank # and colour too, verified across the whole fixture to always carry the # SAME calendar date as the day itself -- a commemoration's own natural # fixed date, by construction, since only a saint impeded on their own day # is ever commemorated there). Never parsed for its date component by this # fixture or the comparator (info.rank/colors already give the day's own # values); kept opaque and compared as a plain string. # - Spaces in tempora are turned to "_" (matching the "no field has an # internal space" convention test/fixtures/lectio-ef-2005-2050.txt already # uses, so this fixture can be read the same simple way -- split on '|', # not on whitespace, so title/commemoration/displaced text keeps its own # spaces; only tempora, which nothing in this project parses further than # pass-through display, is space-collapsed for a cheap column-count check). import json import os import sys def main(): if len(sys.argv) != 3: print(f"usage: {sys.argv[0]} ", file=sys.stderr) return 2 snapshot_dir, out_path = sys.argv[1], sys.argv[2] src = os.path.join(snapshot_dir, "missalemeum", "en") rows = [] for fname in sorted(os.listdir(src)): if not fname.endswith(".json"): continue date = fname[:-5] with open(os.path.join(src, fname), encoding="utf-8") as f: data = json.load(f) first = data[0]["info"] for other in data[1:]: o = other["info"] for k in ("rank", "colors", "tempora", "commemorations", "displaced"): if first[k] != o[k]: print(f"WARNING: {date} masses disagree on {k}: {first[k]!r} vs {o[k]!r}", file=sys.stderr) comm_titles = [c["title"] for c in first["commemorations"]] comm_ids = [c["id"] for c in first["commemorations"]] disp_titles = [d["title"] for d in first["displaced"]] for t in [first["title"]] + comm_titles + comm_ids + disp_titles: if "|" in t or ";" in t: print(f"ERROR: {date}: field {t!r} contains a delimiter this fixture uses", file=sys.stderr) return 1 colors = "".join(sorted(first["colors"])) tempora = (first["tempora"] or "-").replace(" ", "_") comms = ";".join(comm_titles) or "-" disp = ";".join(disp_titles) or "-" ids = ";".join(comm_ids) or "-" rows.append( f"{date}|{first['rank']}|{colors}|{first['title']}|{tempora}|{comms}|{disp}|{len(data)}|{ids}" ) if len(rows) != 730: print(f"ERROR: expected 730 days, got {len(rows)}", file=sys.stderr) return 1 with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(rows) + "\n") print(f"wrote {len(rows)} lines to {out_path}", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())