diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 23:51:36 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 23:51:36 +0200 |
| commit | 0994587b3694bb87d9a8ee32d60babf25c784acf (patch) | |
| tree | 5585419d69e0b89fa839aba94ef50a3297251fbc /tools/extract_of_calendar.py | |
| parent | 98d698b261c75a287ba3992744032babfb6ad763 (diff) | |
| download | colitur-0994587b3694bb87d9a8ee32d60babf25c784acf.tar.gz colitur-0994587b3694bb87d9a8ee32d60babf25c784acf.zip | |
fix(of): correct subject over-match, provenance truth, and stale hazard comment
Four fixes from fix round 2 review:
classify_subject's "maria"/"b.m.v" substring test over-matched 12 saints
who merely carry "Maria" in their own name (Maximilian Mary Kolbe, John
Mary Vianney, Mary Magdalene among them) as Bvm. Replaced with an
explicit BVM_DATES table of the 13 genuinely Marian entries. Bvm 25 -> 13.
The provenance header claimed lectio is "never a source", which omitted
that slugs and English names both come from it. Corrected to state
exactly what does (slugs, en names) and does not (every substantive
field) come from lectio. A missing lectio file used to degrade silently
to a different SHA-256 with no en names and no warning; it now fails
loudly. lectio's own SHA-256 is now pinned alongside the Missal PDF's.
Four more entries carried a second pdftotext artifact distinct from the
letter-spacing already handled -- a combining diacritic rendered as a
stray spacing character (Kim Taegŏn/Chŏng Ha-sang, Đũng Lạc, Makhlūf,
Kęty). Hand-repaired the same way Peter Damian was, each corroborated by
the identical corruption recurring in the Missal's own index.
precedence_of.ml's PHASE 3 HAZARD comment about All Souls (2 November)
was asserting a gap that no longer exists now that OF sanctoral data
ships. Added test_all_souls_beats_an_ordinary_sunday, which resolves the
shipped All Souls entry against a real per-annum Sunday (2025-11-02)
through the actual Precedence.resolve pipeline, and updated the comment
to record the resolution instead of demanding it.
Diffstat (limited to 'tools/extract_of_calendar.py')
| -rw-r--r-- | tools/extract_of_calendar.py | 182 |
1 files changed, 153 insertions, 29 deletions
diff --git a/tools/extract_of_calendar.py b/tools/extract_of_calendar.py index 50b570e..7d7c5aa 100644 --- a/tools/extract_of_calendar.py +++ b/tools/extract_of_calendar.py @@ -6,17 +6,39 @@ typica tertia, into data/of/calendar-2002.sexp: a Colitur_kernel.Layer.t of Colitur_kernel.Celebration.t entries, one per FIXED (month, day) universal calendar entry. -SOURCE DISCIPLINE (do not weaken this): the 2002 Missale Romanum PDF is the -authority. lectio's ~/git/projects/lectio/internal/caldata/roman-calendar.ini -is read here ONLY for two cross-check purposes, never as structural source: - (a) comparing (month, day, rank) to surface divergences, each of which is - adjudicated by re-reading the Missal (see the divergence log this - script prints), never by silently preferring lectio; - (b) copying `name.en` -- lectio's OWN `name.la` field is independently - known to be unreliable (it contains English text for at least one - entry, verified during planning: mary-mother-of-god-octave-of-christmas - has name.la = "Mary, Mother of God (Octave of Christmas)"), so Latin - is NEVER taken from lectio, only English. +SOURCE DISCIPLINE (do not weaken this). The 2002 Missale Romanum PDF is the +SOLE authority for every SUBSTANTIVE field: which day exists at all, its +(month, day), its rank/grade, its colour, its subject, and every character +of its `la` name. None of those is ever taken from, adjusted by, or +defaulted to lectio. + +WHAT ACTUALLY COMES FROM LECTIO, stated exactly rather than summarised +(fix round 2, coordinator, 2026-08-25: a prior version of this note said +lectio is read "ONLY for two cross-check purposes", which undercounted -- +slugs were a third, unlisted use, and are the file's own PRIMARY KEY, not +a minor detail): + (a) `en` names -- lectio's OWN `name.la` field is independently known to + be unreliable (it contains English text for at least one entry, + verified during planning: mary-mother-of-god-octave-of-christmas has + name.la = "Mary, Mother of God (Octave of Christmas)"), so Latin is + NEVER taken from lectio, only English, and only when a confident + per-date match exists (see cross_check's own pairing logic); + (b) SLUGS -- reused verbatim from lectio's own bracket identifier when a + confident match exists (main()'s own "Slugs:" comment explains why + this is not an authority violation: a slug is an engineering key, + not liturgical content); falls back to a mechanical slug built from + the Latin title otherwise; + (c) the (month, day, rank) COMPARISON itself, whose divergences are + listed in this file's own provenance header, each adjudicated by + RE-READING THE MISSAL, never by preferring lectio's answer. +Both (a) and (b) are ENGINEERING/PRESENTATION conveniences layered on top +of Missal-sourced content, not competing sources for that content -- but +they are real dependencies on an external, unpinned-by-Git sibling repo, +which is why lectio's own SHA-256 is now pinned in the emitted provenance +header (below) and why a missing lectio file is now a hard error (see +parse_lectio_ini) rather than a silent zero-entries degradation: running +without lectio present used to exit 0 with a DIFFERENT SHA-256, no `en` +names, and 206 mechanical Latin slugs, with nothing printed to say so. Run: `python3 tools/extract_of_calendar.py [pretext-file] > data/of/calendar-2002.sexp` With no argument, runs `pdftotext -layout` on the PDF itself. A pre-extracted @@ -135,6 +157,7 @@ entries needing this repair is printed to stderr. """ import hashlib +import os import re import subprocess import sys @@ -190,9 +213,18 @@ ALL_SOULS_OVERRIDE = (11, 2) RED_OVERRIDES = {(9, 14)} # subject = Lord: celebrations of the Lord not already excluded above, -# plus the three church-dedication feasts (a dedication is itself -# classified a "festum Domini" -- see CLAUDE.md's EF precedent, and -# lectio's own roman-calendar.ini tags all three `class = lord`). +# plus the three church-dedication feasts. The dedication classification +# is THIS FILE'S OWN READING, not lectio's: CLAUDE.md records a primary- +# source argument for the EF side (the Common of the Dedication of a +# Church is itself titled "Festum Dedicationis Ecclesiae est festum +# Domini" in the Missal) that applies with equal force here -- a +# dedication feast commemorates Christ's own house, not a saint. Fix +# round 2 (coordinator, 2026-08-25): a prior version of this comment cited +# lectio's own `class = lord` tag on these three dates as corroboration, +# which is the wrong way round for a file whose SOLE authority is the +# Missal -- lectio agreeing is worth recording as an observation, never as +# the reason. (It happens to: roman-calendar.ini tags 08-05/11-09/11-18 +# `class = lord`, checked, not merely asserted.) LORD_DATES = {(2, 2), (3, 25), (8, 6), (1, 3), (9, 14)} | DEDICATION_DATES @@ -497,24 +529,70 @@ def classify_colour(month, day, latin): return "White" +# Fix round 2 (coordinator, 2026-08-25): the previous rule ("maria"/"b.m.v" +# substring anywhere in the Latin title) over-matched -- 12 of the 25 +# resulting Bvm entries were saints who happen to carry "Maria" as part of +# their OWN given name (Maximilian MARY Kolbe, John MARY Vianney, Alphonsus +# MARIA de Liguori, MARY Magdalene, MARIA Goretti, Margaret MARY Alacoque, +# Anthony MARY Claret, Anthony MARIA Zaccaria, Louis Grignion de Montfort's +# own "MARIAE", Mary Magdalene de Pazzi) or belong to a religious order whose +# NAME contains "B.M.V." (the Seven Holy Founders of the Servite Order), or +# are Mary's own parents commemorated as themselves (Joachim and Anne) -- +# none of these celebrations is OF Mary; the subject of a celebration is +# whom it commemorates, not who is merely named in its title. Replaced with +# an explicit table of the 12 genuinely Marian FIXED-date entries (read off +# the Missal page by hand, each a feast/memorial of Mary herself), keyed by +# (month, day) rather than slug -- consistent with every other override +# table in this file and independent of lectio's own slug text, which this +# file does not treat as authoritative for anything but the identifier +# itself (see the module docstring's "What actually comes from lectio"). +# The Immaculate Heart of Mary (the one movable Marian entry) is tagged +# separately via MOVABLE_SUBJECT_OVERRIDE, not this table. +BVM_DATES = { + (2, 11): "our-lady-of-lourdes", + (5, 13): "our-lady-of-fatima", + (5, 31): "visitation-of-the-blessed-virgin-mary", + (7, 16): "our-lady-of-mount-carmel", + (8, 15): "assumption-of-the-blessed-virgin-mary", + (8, 22): "queenship-of-blessed-virgin-mary", + (9, 8): "birth-of-the-blessed-virgin-mary", + (9, 12): "holy-name-of-the-blessed-virgin-mary", + (9, 15): "our-lady-of-sorrows", + (10, 7): "our-lady-of-the-rosary", + (11, 21): "presentation-of-the-blessed-virgin-mary", + (12, 8): "immaculate-conception-of-the-blessed-virgin-mary", +} + + def classify_subject(month, day, latin): key = (month, day) if key in LORD_DATES: return "Lord" - lower = normalize_latin(latin).lower() - if key != (3, 19) and ("maria" in lower or "b.m.v" in lower or "bmv" in lower): + if key in BVM_DATES: return "Bvm" return "Saint" def parse_lectio_ini(path): + # Fix round 2: this used to catch FileNotFoundError and return [], + # letting main() run to completion with zero lectio-derived data (no + # en names, mechanical Latin slugs) and exit 0 -- SILENTLY, with a + # DIFFERENT SHA-256 than the shipped file, and nothing printed to + # explain why. lectio is a real, load-bearing dependency for slugs and + # en names (see the module docstring's "WHAT ACTUALLY COMES FROM + # LECTIO"), so its absence is now fatal, not degraded-and-silent. + if not os.path.exists(path): + raise SystemExit( + f"extract_of_calendar: lectio's cross-check file is required and was not found: " + f"{path!r}. Running without it would silently change the output (no `en` names, " + f"mechanical Latin slugs instead of lectio's, a different SHA-256) with no warning " + f"-- exactly the failure mode fix round 2 closed. Clone/update " + f"~/git/projects/lectio and re-run." + ) + with open(path, encoding="utf-8") as f: + raw = f.read() entries = [] cur = None - try: - with open(path, encoding="utf-8") as f: - raw = f.read() - except FileNotFoundError: - return entries for line in raw.split("\n"): line = line.rstrip("\n") if line.startswith("[") and line.endswith("]"): @@ -635,23 +713,34 @@ def render_sexp(entries, name_en, letterspace_count, skipped, meta): lines = [] lines.append(f"; data/of/calendar-2002.sexp -- OF (post-1970) General Roman Calendar,") lines.append(f"; transcribed from the Missale Romanum, editio typica tertia (2002). The") - lines.append(f"; 2002 Missal is the AUTHORITY; this file is never edited afterwards --") - lines.append(f"; spec sec4.1. lectio's roman-calendar.ini is a cross-check only, never a") - lines.append(f"; source (see tools/extract_of_calendar.py's own header for the full") - lines.append(f"; source-discipline statement and every classification citation).") + lines.append(f"; 2002 Missal is the SOLE authority for every substantive field (date, rank,") + lines.append(f"; colour, subject, every character of `la`); this file is never edited") + lines.append(f"; afterwards -- spec sec4.1. lectio's roman-calendar.ini supplies `en` names") + lines.append(f"; and, when a confident match exists, the slug (an engineering key, not") + lines.append(f"; content -- see tools/extract_of_calendar.py's own docstring, \"WHAT") + lines.append(f"; ACTUALLY COMES FROM LECTIO\", for exactly what and why) -- corrected fix") + lines.append(f"; round 2, 2026-08-25: a prior header here said lectio was \"never a") + lines.append(f"; source\", which omitted slugs and was not quite true.") lines.append(f";") lines.append(f"; Generator: {TOOL_PATH} -- do not hand-edit; re-run against the same") - lines.append(f"; PDF (this file is pinned, so a re-run should reproduce it byte-for-byte)") - lines.append(f"; and diff before committing.") + lines.append(f"; PDF AND the same lectio snapshot (its SHA-256 is pinned below; a missing") + lines.append(f"; or different lectio file now fails loudly rather than silently changing") + lines.append(f"; slugs/en-names/SHA-256 -- fix round 2) to reproduce this file byte-for-") + lines.append(f"; byte, and diff before committing.") lines.append(f";") - lines.append(f"; Source: {PDF_PATH}") + lines.append(f"; Source (authority): {PDF_PATH}") lines.append(f"; SHA-256: {meta['pdf_sha256']}") lines.append(f"; Extracted lines (pdftotext -layout, 0-based): {meta['start']}-{meta['end']}") + lines.append(f"; Source (slugs + en names only, never content -- see above): {LECTIO_INI}") + lines.append(f"; SHA-256: {meta['lectio_sha256']}") lines.append(f"; Extraction date (UTC): {meta['extraction_date']}") lines.append(f";") lines.append(f"; {len(entries)} entries. {letterspace_count} entry needed letter-spacing repair") lines.append(f"; (collapse_letterspacing's own mechanical pass, then hand-verified -- see") - lines.append(f"; the module docstring's EXTRACTION ARTIFACT note).") + lines.append(f"; the module docstring's EXTRACTION ARTIFACT note); {meta['diacritic_repair_count']}") + lines.append(f"; more carry a SEPARATE pdftotext artifact (a combining diacritic rendered") + lines.append(f"; as a stray spacing character), also hand-repaired -- fix round 2, see") + lines.append(f"; main()'s own diacritic_repairs table.") lines.append(f";") lines.append(f"; Excluded, deliberately (see module docstring for the full reasoning):") for (m, d), why in sorted(skipped.items()): @@ -746,6 +835,36 @@ def main(): if (e["month"], e["day"]) == (2, 21) and "episcopiet" in e["latin"]: e["latin"] = "S. Petri Damiani, episcopi et Ecclesiae doctoris" + # Fix round 2 (coordinator, 2026-08-25): a SECOND, different pdftotext + # artifact -- distinct from the letter-spacing above -- affects four + # more entries. Each is a proper noun carrying a COMBINING diacritic + # (macron/breve/tilde/ogonek/dot-below) the font's cmap maps to a + # SEPARATE spacing character positioned after its base letter with a + # stray space, instead of combining onto it (e.g. "Makhlu‾ f" where + # ‾ is U+203E OVERLINE standing in for a macron over "u"). Not + # mechanically recoverable the way collapse_letterspacing's own runs + # are (there is no generic rule for "which glyph substitutes for which + # combining mark"), so each is hand-repaired here, exactly like Peter + # Damian above -- and each is corroborated by the IDENTICAL corruption + # recurring a second time elsewhere in the same extracted text (the + # Missal's own alphabetical index of saints, lines ~33857/34079: the + # stray mark pairs with the same base letter both times, not assumed + # from outside knowledge alone), then checked against the standard + # Latin liturgical/romanised spelling for each canonised saint. + diacritic_repairs = [ + (7, 24, "Makhlu‾ f", "Makhlūf"), # OVERLINE -> u-macron: Sarbelii Makhlūf + (9, 20, "Tae-go¡ n", "Tae-gŏn"), # INV. EXCL. MARK -> o-breve: Kim Taegŏn + (9, 20, "Cho¡ ng", "Chŏng"), # same artifact, same entry: Chŏng Ha-sang + (11, 24, "Du˜ ng La.c", "Dũng Lạc"), # TILDE -> u-tilde; "."-> a-dot-below: Dũng Lạc + (12, 23, "Ke¸ty", "Kęty"), # CEDILLA -> e-ogonek: Ioannis de Kęty + ] + diacritic_repair_count = 0 + for e in entries: + for m, d, bad, good in diacritic_repairs: + if (e["month"], e["day"]) == (m, d) and bad in e["latin"]: + e["latin"] = e["latin"].replace(bad, good) + diacritic_repair_count += 1 + skipped = {} kept = [] for e in entries: @@ -767,6 +886,7 @@ def main(): e["subject"] = MOVABLE_SUBJECT_OVERRIDE[e["easter_offset"]] lectio_entries = parse_lectio_ini(LECTIO_INI) + lectio_sha256 = sha256_file(LECTIO_INI) cross_check_report, name_en, lectio_slug = cross_check(entries, lectio_entries) # Slugs: prefer lectio's OWN identifier when a confident match exists -- @@ -807,19 +927,23 @@ def main(): meta = { "pdf_sha256": sha256_file(PDF_PATH), + "lectio_sha256": lectio_sha256, "start": start, "end": end, "extraction_date": datetime.now(timezone.utc).strftime("%Y-%m-%d"), "cross_check_report": cross_check_report, "movable_entries": movable_entries, + "diacritic_repair_count": diacritic_repair_count, } sys.stdout.write(render_sexp(entries, name_en, letterspace_count, skipped, meta)) print(f"[extract_of_calendar] {len(entries)} entries emitted ({len(movable_entries)} of them " f"movable, Easter_offset), {letterspace_count} needed letter-spacing repair, " + f"{diacritic_repair_count} needed diacritic repair, " f"{len(skipped)} dates excluded (temporal_of.ml coverage)", file=sys.stderr) + print(f"[extract_of_calendar] lectio SHA-256: {lectio_sha256}", file=sys.stderr) print(f"[extract_of_calendar] MOVABLE-HEADING AUDIT: {len(movable_audit)} \"Dominica/Feria/" f"Sabbato ... :\" headings found in the table:", file=sys.stderr) for ma in movable_audit: |
