diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 04:36:36 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 04:36:36 +0200 |
| commit | cb200a3f95e24f67c1d08b2ceb09404bf57a1636 (patch) | |
| tree | 7a885ed0d16e945a5f9d0338752d8611221227ea /tools | |
| parent | c464c0435c650cf019eb6c6ae1906217d557a15f (diff) | |
| download | colitur-cb200a3f95e24f67c1d08b2ceb09404bf57a1636.tar.gz colitur-cb200a3f95e24f67c1d08b2ceb09404bf57a1636.zip | |
test: oracle vs missalemeum 2026-2027; audit the sanctoral
Validation layer 4: an oracle harness against missalemeum (Divinum
Officium data), independent of the lectio bootstrap chain colitur's
own sanctoral data comes from -- the only layer that can catch an
error inherited from that bootstrap, and the only one that can
validate commemorations at all (the lectio differential explicitly
excludes them, per its own header comment).
tools/extract_missalemeum_oracle.py shapes the fixture from lectio's
sources/snapshot.tar.gz outside the test (no JSON library in this
project's frozen deps, same reasoning test_differential.ml's own
fixture already documents). test/fixtures/missalemeum-ef-2026-2027
.txt (730 days, SHA-256 pinned and asserted) + its own .provenance
note record exactly how to regenerate it.
test_oracle.ml compares three axes the oracle actually supports:
rank, colour (SET MEMBERSHIP -- 14 of 730 days carry two colours,
e.g. rose+violet on Gaudete/Laetare, which independently vindicates
this project's own rose reading against lectio's violet-only one,
recorded in the register), and commemoration presence/count. Slug
identity is deliberately out of scope (needs a title->slug mapping,
the data audit's own business, not the automated comparator's).
Of 730 days, 688 matched cleanly outright. The remaining 42 are all
named in data/ef/expected-divergences-missalemeum.sexp (14 cited
entries, M1-M14): most are genuine primary-source-confirmed findings
this task adjudicated and fixed in the two preceding commits (RG 33,
RG 109/111, Holy Thursday's colour); the rest are real, cited,
deferred feature/data gaps (RG 91 entry 27's BVM-Saturday office, RG
110's inseparable Peter/Paul commemoration, four sanctoral entries
missing from lectio's own source) or genuine oracle-side artifacts
-- honestly verdicted against whichever side this task's own
primary-source research actually backs, never defaulted to colitur.
One entry (M13, St Joseph vs the Friday of Passion Week 2027) is
verdict open: adjudicated as unresolved after real search effort,
not guessed past.
The data audit: every sanctoral entry the comparison flagged was
hand-checked against the 1962 calendarium, plus a 20-entry
deterministic random control sample (seed 20260812) drawn
independently of the flagged set. The control sample caught two
entries (benedict, frances-rome) marked Commemoration_only in the
bootstrapped data when the primary calendarium lists them as plain
III-class feasts with their own Office -- traced to lectio's own
source, not fixable here, and reported as a signal (10% of a random
sample) rather than a blanket claim. Coverage recorded honestly in
docs/research/rules-register.md's own three buckets: confirmed by
oracle (200/322), confirmed by hand (23/322, 2 of them wrong),
unverified (115/322) -- the unverified bucket stated explicitly
rather than left implicit.
Harness teeth demonstrated and reverted (not committed): a fixture
rank/colour edit on a previously-clean day fails both the checksum
pin and the no-unexplained-differences assertion independently; an
expected_rows drift on the allow-list fails the citation-count
assertion. Both captured with their exact failure messages, both
reverted before this commit.
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/extract_missalemeum_oracle.py | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/tools/extract_missalemeum_oracle.py b/tools/extract_missalemeum_oracle.py new file mode 100644 index 0000000..044aa54 --- /dev/null +++ b/tools/extract_missalemeum_oracle.py @@ -0,0 +1,87 @@ +#!/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 +# +# - 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). +# - 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"]] + disp_titles = [d["title"] for d in first["displaced"]] + for t in [first["title"]] + comm_titles + 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 "-" + rows.append(f"{date}|{first['rank']}|{colors}|{first['title']}|{tempora}|{comms}|{disp}|{len(data)}") + + 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()) |
