1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
#!/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 <snapshot-dir> <out-file>
#
# <snapshot-dir> 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 <snapshot-dir>/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]} <snapshot-dir> <out-file>", 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())
|