#!/usr/bin/env python3 """Emit the minimal set of field changes that bring lectio's EF data into agreement with colitur, each annotated with the rubric that justifies it. DELIBERATELY A PATCH, NOT A REGENERATION. colitur carries 205 of the 327 Polish names lectio ships, so regenerating lectio's ini from colitur would silently drop 122 of them. It also uses a different slug vocabulary for Passion/Holy week and a different Paschaltide week numbering. What colitur is authoritative for is the adjudicated FIELDS -- rank/status, colour, and the reading citations it has verified against the Missal -- and this emits only those. Every line of output names the colitur allow-list entry (C-class) carrying the citation, so a lectio reviewer can check the rubric rather than trust the tool. KNOWN BLIND SPOT, found the hard way. This compares colitur's SANCTORAL data against lectio's calendar ini, so it can only see corrections that live in DATA. A correction to a TEMPORAL day -- computed in code on both sides, present in neither file -- is structurally invisible to it. Not hypothetical: the Good Friday colour fix (RG 128(b) + RG 132, colitur v0.4.0) was missed entirely here, and surfaced only by running clectio at the BOTTOM of the chain and diffing its output against colitur date by date. This tool narrows the search; it does not close it. Before trusting a clean run, diff the two engines' actual OUTPUT for the days you care about. """ import re, sys, os COL = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) LEC = os.path.expanduser('~/git/projects/lectio/internal/caldata') def parse_ini(path): out, cur = {}, None for line in open(path, errors='replace'): s = line.strip() m = re.match(r'^\[([\w-]+)\]$', s) if m: cur = m.group(1); out[cur] = {}; continue if cur and '=' in s and not s.startswith((';', '#')): k, v = s.split('=', 1); out[cur][k.strip()] = v.strip() return out def colitur_sanctoral(): t = open(f'{COL}/data/ef/sanctoral.sexp').read() out = {} for m in re.finditer(r'\(\(slug ([\w-]+)\)(.*?)\(layer', t, re.S): slug, blk = m.group(1), m.group(2) g = lambda k: (re.search(r'\(' + k + r' (\w+)\)', blk) or [None, None])[1] out[slug] = {'rank': g('rank'), 'status': g('status'), 'colour': g('colour'), 'subject': g('subject')} return out def colitur_lectionary(): t = open(f'{COL}/data/ef/lectionary.sexp').read() out = {} for m in re.finditer(r'\(([a-z0-9-]+)\s*\(((?:\s*\(\(part \w+\) \(reference "[^"]*"\)\))+)\s*\)\)', t): p = dict(re.findall(r'\(part (\w+)\) \(reference "([^"]*)"\)', m.group(2))) out[m.group(1)] = {'first': p.get('First'), 'gospel': p.get('Gospel')} return out RANKMAP = {'Class1': 'class-1', 'Class2': 'class-2', 'Class3': 'class-3', 'Class4': 'class-4'} # why each field change is being made, keyed by slug WHY = { 'ubaldus': ('C38', 'Missal calendarium: "S. Ubaldi Ep. et Conf., III classis" -- a feast, not a commemoration'), 'didacus': ('C38', 'Missal calendarium: "S. Didaci Conf., III classis" -- a feast, not a commemoration'), 'vigil-of-st-lawrence': ('C37', 'RG 128: violet for vigils of II and III class outside Paschaltide'), 'vigil-of-the-assumption': ('C37', 'RG 128: violet for vigils of II and III class outside Paschaltide'), 'ef-advent-ember-sat': ('C27', 'lectio carries St Thomas the Apostle\'s Mass under this key; the real Advent Ember Saturday Mass is 2 Thess 2:1-8 / Luke 3:1-6'), 'ef-september-ember-sat': ('C28', 'lectio carries St Matthew\'s Mass under this key; the real September Ember Saturday Mass is Heb 9:2-12 / Luke 13:6-17'), } def main(): lec_cal = parse_ini(f'{LEC}/tridentine-calendar.ini') lec_lec = parse_ini(f'{LEC}/tridentine-lectionary.ini') col_cal, col_lec = colitur_sanctoral(), colitur_lectionary() changes = [] for slug in sorted(set(lec_cal) & set(col_cal)): l, c = lec_cal[slug], col_cal[slug] want_rank = 'commemoration' if c['status'] == 'Commemoration_only' else RANKMAP.get(c['rank']) if l.get('rank') != want_rank: changes.append(('tridentine-calendar.ini', slug, 'rank', l.get('rank'), want_rank)) if c['colour'] and l.get('colour', '').lower() != c['colour'].lower(): changes.append(('tridentine-calendar.ini', slug, 'colour', l.get('colour'), c['colour'].lower())) for slug in sorted(set(lec_lec) & set(col_lec)): for k in ('first', 'gospel'): if col_lec[slug].get(k) and lec_lec[slug].get(k) != col_lec[slug][k]: changes.append(('tridentine-lectionary.ini', slug, k, lec_lec[slug].get(k), col_lec[slug][k])) print(f"# {len(changes)} field changes bringing lectio's EF data into agreement with colitur") print(f"# Generated by colitur tools/export_lectio_patch.py -- review, do not apply blind.\n") cur = None for f, slug, key, old, new in changes: if f != cur: print(f"\n## {f}"); cur = f cid, why = WHY.get(slug, ('?', 'no citation recorded -- INVESTIGATE before applying')) print(f" [{slug}]") print(f" {key}: {old!r} -> {new!r}") print(f" why ({cid}): {why}") only_col = sorted(set(col_lec) - set(lec_lec)) print(f"\n## lectionary entries colitur has and lectio lacks: {len(only_col)}") print(" (additions, not corrections -- listed for review, not emitted as a patch)") for s in only_col[:8]: print(f" {s}") if len(only_col) > 8: print(f" ... and {len(only_col)-8} more") main()