aboutsummaryrefslogtreecommitdiff
path: root/tools/extract_missalemeum_oracle.py
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-17 12:28:03 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-17 12:28:03 +0200
commit8d1eb25a311e16ddb7bf89d2b21fb1ca0aa058a7 (patch)
tree9c72d1c30f0f37ef7138325abb3ca26ebe68ff14 /tools/extract_missalemeum_oracle.py
parent9947a28f3a40bd8ac36892dcf5aad59d9e6f847c (diff)
downloadcolitur-8d1eb25a311e16ddb7bf89d2b21fb1ca0aa058a7.tar.gz
colitur-8d1eb25a311e16ddb7bf89d2b21fb1ca0aa058a7.zip
test(oracle): compare reading citations against missalemeum
Layer 4 (test_oracle.ml) now extracts and compares the Epistle/Gospel citation for every one of the 730 days in the 2026-2027 fixture, the same axis Task 8 already added to layer 3. tools/ extract_missalemeum_oracle.py gained a structural citation-span detector (the first "*...*"-wrapped span under 40 chars containing a chapter:verse-shaped digit pair), verified exhaustively over all 1 458 Lectio/Evangelium sections. 728 of 730 days are comparable; the 2 that are not (Good Friday, both years) have no such section in missalemeum's own JSON at all -- a structural limit on the oracle's own side (M23), not a colitur gap. 451 of 730 days differ and are all explained across 19 cited entries; 0 unexplained. Chain step 4 (the Commons) has no layer-4 witness: confirmed empirically (a scratch sweep, not merely re-asserted from Task 6's own report) that none of the 15 Commons/proper saints is ever the observed office in this window. The comparison surfaced three genuine colitur bugs in data/ef/lectionary.sexp, all fixed at the source in tools/bootstrap_lectionary.ml: 1. Holy Name of Jesus (ef-holy-name-sunday/ef-holy-name) carried a mis-derived citation, borrowed by analogy from an unrelated ini section ("ef-christmas-sunday-0", the Sunday-within-the-Octave's own Mass) on the wrong assumption the two share a Mass. Disproved by missalemeum's own extraction for both real Holy Name Sundays in its window, confirmed against the Missal itself, twice, both scans (Acts 4:8-12/Luke 2:21, not Gal 4:1-7/Luke 2:33-40). 2. RG 17(a)'s own "diebus ferialibus" rubric (immediately following the Holy Name heading, both scans) was never implemented: 2-5 January ferias repeat 1 January's Mass, 7-12 January ferias before Holy Family Sunday repeat Epiphany's, and Holy Family's own week has a named formula ("Missa Dominicae Primae post Epiphaniam"). All three fell to step 3's undifferentiated Sunday walkback instead. Fixed for three 5-weekday families, deliberately excluding Saturday from all three -- RG 78's BVM Saturday Office wins there unconditionally, confirmed against missalemeum's own title on this window's own such Saturdays. 3. Corpus Christi and Sacred Heart (both real, never-impeded I-class movable feasts) had no lectionary entry at all. Both engines independently fell back to Trinity Sunday's own citation and silently agreed on the wrong answer -- the "Holy Thursday violet in both" shape CLAUDE.md names for exactly this reason. Because lectionary.sexp is shared with layer 3 (the lectio differential, 2005-2050), these fixes changed colitur's output for the whole domain, not only 2026-2027. data/ef/expected-divergences.sexp and test_differential.ml needed matching updates: C1 narrowed 361->173 and C25 narrowed 57->13 (rows that now resolve correctly moved to new entries), C16's note corrected (count unchanged), and six new entries added (C29-C34, 397 rows total, all verdict colitur). test_lectionary.ml and test_lectionary_ef.ml's own pins were updated to match (entry count 131->148; one step-3 regression test re-pointed to a date that still genuinely exercises that step). One entry, M26 (30 rows, verdict open), is a genuine unfixed gap: colitur's step-3 "resume the preceding Sunday" fallback is its only ferial-Mass mechanism, and missalemeum's own selection is more granular in three confirmed ways (a rotating votive Mass of the BVM, a named weekly formula for at least one week, and later Time-after-Pentecost weeks this task's own investigation did not narrow to a single rule) -- the same "needs a season-keyed selection mechanism, new behaviour not new data" shape data/ef/expected-divergences.sexp's own C6/C25 and Task 6's own residual concern already name, out of this task's own scope to build. dune test: 358 tests green. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force: 359 tests green, 89.5s.
Diffstat (limited to 'tools/extract_missalemeum_oracle.py')
-rw-r--r--tools/extract_missalemeum_oracle.py58
1 files changed, 57 insertions, 1 deletions
diff --git a/tools/extract_missalemeum_oracle.py b/tools/extract_missalemeum_oracle.py
index a010b52..4b010e2 100644
--- a/tools/extract_missalemeum_oracle.py
+++ b/tools/extract_missalemeum_oracle.py
@@ -18,7 +18,7 @@
# exists for every day.
#
# One line per day, pipe-separated:
-# date|rank|colors|title|tempora|commemorations|displaced|n_masses|commemoration_ids
+# date|rank|colors|title|tempora|commemorations|displaced|n_masses|commemoration_ids|first|gospel
#
# - rank/colors/title/tempora/commemorations/displaced are info.rank,
# info.colors (sorted, concatenated, e.g. "pv"), info.title, info.tempora
@@ -42,6 +42,35 @@
# 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.
+# - first/gospel (Task 9) are the day's own Epistle/Lesson and Gospel
+# citations, extracted from entry[0]["sections"] -- the section with
+# id "Lectio" ("Epistle") and "Evangelium" ("Gospel") respectively. Each
+# section's own body is a [[english, latin]] pair; the ENGLISH text
+# (body[0][0]) embeds the citation as one of possibly several
+# "*...*"-wrapped (markdown-italic) spans -- e.g. "Lesson from the letter
+# of St. Paul... \n*Titus 2:11-15*\nBeloved: ...". It is NOT always the
+# FIRST such span: Holy Saturday's own Gospel section opens with a rubric
+# note, itself "*"-wrapped ("*While singing the Gospel candles are not
+# being hold.*"), before the real citation "*Matt 28:1-7*" -- checked
+# directly against the raw JSON, not assumed. The citation span is
+# identified structurally instead of positionally: the first "*...*" span
+# under 40 characters that contains a chapter:verse-shaped digit pair
+# (`\d+\s*[:,.]\s*\d+`, matching all three separators actually used in
+# this source -- "Titus 2:11-15", "4 Kings, 5:1-15", "John 20. 19-31").
+# Verified exhaustively over the whole fixture (all 1 458 Lectio/
+# Evangelium sections across the 728 days that carry one): exactly one
+# candidate per section, zero ambiguous, zero false positives from a
+# longer rubric sentence. TWO days (Good Friday, both years -- the "Missa
+# Praesanctificatorum" liturgy) have no "Lectio"/"Evangelium" section at
+# all (a multi-lesson structure instead, "Lectiones"/"Passio", with no
+# single reading occupying the Epistle/Gospel slot this schema assumes,
+# the SAME shape colitur's own test_lectionary.ml records for its own
+# hand-authored Holy Week data) -- "-" for both fields on those two rows,
+# a WARNING on stderr naming the date, never a silent guess. Extracted
+# VERBATIM (only .strip()ped of surrounding whitespace) -- not normalized,
+# not re-punctuated: the comparator's job, not this extractor's, matching
+# the same "verbatim, not translated or slugified" discipline the fields
+# above already state.
# - 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 '|',
@@ -50,8 +79,27 @@
# pass-through display, is space-collapsed for a cheap column-count check).
import json
import os
+import re
import sys
+STAR_RE = re.compile(r"\*([^*]+)\*")
+VERSE_RE = re.compile(r"\d+\s*[:,.]\s*\d+")
+
+
+def extract_citation(sections, section_id, date, label):
+ for sec in sections:
+ if sec["id"] != section_id:
+ continue
+ text = sec["body"][0][0]
+ candidates = [s.strip() for s in STAR_RE.findall(text) if VERSE_RE.search(s) and len(s) < 40]
+ if candidates:
+ return candidates[0]
+ print(f"WARNING: {date}: {section_id!r} section found but no citation-shaped span in it ({label})",
+ file=sys.stderr)
+ return "-"
+ print(f"WARNING: {date}: no {section_id!r} section ({label})", file=sys.stderr)
+ return "-"
+
def main():
if len(sys.argv) != 3:
@@ -84,8 +132,16 @@ def main():
comms = ";".join(comm_titles) or "-"
disp = ";".join(disp_titles) or "-"
ids = ";".join(comm_ids) or "-"
+ sections = data[0]["sections"]
+ cit_first = extract_citation(sections, "Lectio", date, "Epistle")
+ cit_gospel = extract_citation(sections, "Evangelium", date, "Gospel")
+ for t in (cit_first, cit_gospel):
+ if "|" in t:
+ print(f"ERROR: {date}: citation {t!r} contains a delimiter this fixture uses", file=sys.stderr)
+ return 1
rows.append(
f"{date}|{first['rank']}|{colors}|{first['title']}|{tempora}|{comms}|{disp}|{len(data)}|{ids}"
+ f"|{cit_first}|{cit_gospel}"
)
if len(rows) != 730: