aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 12:57:18 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 12:57:18 +0200
commit084ff22997af208715656924d9fc23bf2ec10050 (patch)
tree882924bffbdffba491b1c5a62cc24fba3609d657
parent4d143d762713783d35b2b1d20f8a21d79884dafe (diff)
downloadcolitur-084ff22997af208715656924d9fc23bf2ec10050.tar.gz
colitur-084ff22997af208715656924d9fc23bf2ec10050.zip
feat(tools): export the lectio EF correction patch
colitur has been downstream of lectio since its data was bootstrapped from it. These are the fields where that relationship should now invert: eight changes, each annotated with the colitur allow-list entry carrying its rubric. Deliberately a PATCH and 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; colitur also uses a different slug vocabulary for Passion/Holy week and different Paschaltide week numbering. What colitur is authoritative for is the adjudicated fields, and only those are emitted. Comparing the two datasets field by field also caught a counting error in colitur's own register that no assertion in this project was watching for: six sanctoral entries carry subject = Lord, not the four recorded, because the regex that counted them truncated on the two longest Polish names. The RG 112(b) reachability measurement was re-run against all six and is unchanged at 0/0; both docs are corrected.
-rw-r--r--CLAUDE.md2
-rw-r--r--tools/export_lectio_patch.py91
2 files changed, 92 insertions, 1 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index d38fbb8..0c0272d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -623,7 +623,7 @@ returns one office per day, so two *de Tempore* candidates never coexist;
0 days domain-wide. **(b) is already produced by RG 16(a)** — 0 days either
direction across 1583–9999, because the feast takes the Sunday's place
*"nulla fit commemoratio"* and no Class1 Sunday shares a date with any of the
-4 Lord-subject entries. **(d)'s saint half has no candidate pair** — the only
+6 Lord-subject entries. **(d)'s saint half has no candidate pair** — the only
genuine one, `agnes`/`agnes-secundo`, is 7 days apart and co-occurs 0 times.
So (b) and (d) are DATA-unreachable, not architecture-unreachable, and an
`--overlay` can make either live — the `entry_14_movable_band` shape. Whether
diff --git a/tools/export_lectio_patch.py b/tools/export_lectio_patch.py
new file mode 100644
index 0000000..de8a208
--- /dev/null
+++ b/tools/export_lectio_patch.py
@@ -0,0 +1,91 @@
+#!/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.
+"""
+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()