summaryrefslogtreecommitdiff
path: root/tools/export_lectio_patch.py
blob: 9942f9cc746cba2e5c905a7005147e075c9bcb29 (plain) (blame)
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
"""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()