aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/gen-sanctoral-ef.go264
1 files changed, 180 insertions, 84 deletions
diff --git a/scripts/gen-sanctoral-ef.go b/scripts/gen-sanctoral-ef.go
index 75ee70d..0812472 100644
--- a/scripts/gen-sanctoral-ef.go
+++ b/scripts/gen-sanctoral-ef.go
@@ -10,8 +10,10 @@
// (Lectio) and Gospel (Evangelium) citations, plus any sancti co-celebrations
// listed as commemorations. Several reference years are tried per date so a
// saint whose date is a Sunday (or under a higher feast) in one year is still
-// captured, observed with its own readings, from another. Existing Latin names
-// in the file are preserved (missalemeum has no Latin titles).
+// captured, observed with its own readings, from another. Existing
+// non-English names (Polish, Latin, or any other language already present
+// in the file) are preserved verbatim -- missalemeum supplies English
+// titles only.
//
// One-time; requires network. Run from the repo root:
//
@@ -123,32 +125,54 @@ func rankOf(n int) calendar.Rank {
// this function produced -- but "purification" is a THIRD, DELIBERATELY
// DIFFERENT case, kept matching rather than narrowed, and the reason is
// itself worth recording:
+//
// - "purification" IS kept as a lord match, even though the calendarium's
// own title is "IN PURIFICATIONE B. MARIAE VIRG." (a feast of the BLESSED
-// VIRGIN by name). The tag's actual job here is not naming/colour
-// categorisation -- it exists solely to drive the occurrence rule
-// ("a II-class feast of the Lord takes an occurring Sunday's place
-// outright"), and on THAT question the calendarium's title is not the
-// decisive evidence: missalemeum -- the same oracle this generator's own
-// data is bootstrapped from -- shows the Purification taking a II-class
-// Sunday's place OUTRIGHT with commemorations EMPTY (confirmed live,
-// 2014-02-02, 2020-02-02, 2025-02-02), the exact "festum Domini" pattern,
-// not the ordinary-BVM-feast pattern (control: the Nativity of the BVM,
-// 8 September, on a Sunday -- 2019-09-08 -- shows the SUNDAY observed,
-// the feast merely commemorated, the opposite shape). An earlier version
-// of this comment (and this codebase) untagged it on the calendarium's
-// title alone; that was reversed after the same live check colitur's own
-// review independently ran (colitur git history: cab8b07 retags it BVM,
-// then 7d3b5ec reverses that "follow the oracle... a different project
-// could reasonably rule the other way on the same evidence" -- recorded
-// as a genuinely contested point, not a clean-cut error, but the
-// occurrence-behaviour evidence is what this tag is FOR).
+// VIRGIN by name) -- a DECISION AGAINST that primary text, made on
+// occurrence-behaviour evidence, not a reading of it, and recorded as
+// such rather than dressed up as textually clean.
+//
+// The tag's actual job is not naming/colour categorisation: it exists
+// solely to drive the occurrence rule that separates RG 91 entry 14
+// ("Festa Domini II classis" -- takes an occurring II-class Sunday's
+// place outright) from entry 16 ("Festa II classis Ecclesiae universae,
+// quae non [sunt Domini]" -- merely commemorated on one), entry 15
+// ("Dominicae II classis") sitting between the two. Checked live against
+// missalemeum (this generator's own data source): the Purification
+// takes a II-class Sunday's place OUTRIGHT, commemorations EMPTY
+// (2014-02-02, 2020-02-02, 2025-02-02, 2031-02-02, 2042-02-02, all
+// fetched independently) -- entry 14's pattern, not entry 16's (control:
+// the Nativity of the BVM, an undisputed ordinary Marian feast, on a
+// Sunday -- 2019-09-08 -- shows the SUNDAY observed, the feast merely
+// commemorated, entry 16's own pattern). RG 112(b), "Officium, Missa aut
+// commemoratio de dominica excludit commemorationem... de festo vel
+// mysterio Domini, et vicissim" (a Sunday's office and a feast/mystery
+// OF THE LORD mutually exclude each other as commemorations), backs the
+// empty commemoration list independently of RG 91's table position.
+//
+// Genuine primary-text counter-evidence exists and is not discarded:
+// RG 120(b), "Adhibetur color albus... b) B. Mariae Virg., etiam in
+// benedictione et processione candelarum die 2 februarii" -- 2 February
+// is filed under the WHITE-colour rule's "B. Mariae Virg." heading,
+// kept separate there from 120(a)'s own "Domini" heading. Both textual
+// tests available (the calendarium's title, RG 120's own taxonomy)
+// point BVM; the occurrence-behaviour evidence points Domini. This is a
+// live, acknowledged disagreement with the primary text on the
+// strength of oracle evidence about what the day actually DOES, not a
+// claim that the text is wrong or ambiguous. See
+// internal/calendar/precedence_ef_repro_test.go's
+// TestPurificationBeatsFebruarySunday for the fixture this rests on --
+// committed, not merely asserted, since the deciding years (2 February
+// on a Sunday) fall outside this repo's own 2026-2027 oracle snapshot
+// window.
+//
// - "holy name" alone is ambiguous: it matches BOTH "Holy Name of Jesus"
// (a feast of the Lord) and "Most Holy Name of Mary"/"Holy Name of Mary"
// (a feast of the BVM, calendarium: "Sanctissimi Nominis Mariae") --
// wrongly matching the latter too. Excluded whenever the title also
// names Mary. Unlike the Purification, this one is NOT contested: its
// occurrence behaviour matches the ordinary-BVM pattern too.
+//
// - "baptism" is a new case: "Commemoration of the Baptism of the Lord" (13
// January, calendarium: "IN COMMEMORATIONE BAPTISMATIS D. N. I. C.") did
// not match any existing case -- the HasSuffix check below requires "of
@@ -240,9 +264,16 @@ func fetch(date string) (*mmDay, error) {
}
type entry struct {
- slug, date, colour, class, en, la, first, gospel string
- rank calendar.Rank
- observed bool // has readings / reliable rank
+ slug, date, colour, class, en, first, gospel string
+ rank calendar.Rank
+ observed bool // has readings / reliable rank
+ // otherNames holds every "name.<lang>" field already present in the
+ // existing file for this slug, EXCLUDING "name.en" (English always
+ // comes fresh from missalemeum, in `en` above). Keyed by the full
+ // field name (e.g. "name.pl") so main() can emit it verbatim without
+ // hardcoding a language list -- see main()'s own preservedNames
+ // comment for why a hardcoded list is exactly the bug this fixes.
+ otherNames map[string]string
}
// idParts splits "sancti:MM-DD[sfx]:RANK:COLOUR" into rank word and colour word.
@@ -300,21 +331,25 @@ func readingsFrom(d *mmDay) (first, gospel string) {
return
}
-// knownSpuriousComm excludes specific missalemeum commemorations, keyed
-// "MM-DD/slug", that the 1962 calendarium's own row for that date does NOT
-// support -- confirmed by checking the primary text directly, not inferred.
-// 9 August's row reads only "Vigilia, III classis.", with no "Com." line, so
-// missalemeum's own "St. Romanus" commemoration that day has no calendarium
-// backing. This is a genuine upstream (missalemeum) data quirk, not something
-// derivable from the API response itself, so it is recorded here rather than
-// silently reproduced -- see the report for the primary-source citation.
-var knownSpuriousComm = map[string]bool{
- "08-09/romanus": true,
- // 14 August's row in the calendarium reads only "Vigilia, II classis.",
- // no "Com." line; the genuine St Eusebius (Bishop and Martyr) is
- // commemorated 16 December instead, where he is correctly present.
- "08-14/eusebius": true,
-}
+// PRIMARY-SOURCE NOTE (found and corrected during review): of the three
+// local scans this generator's citations are checked against
+// (docs/research/*.pdf in the sibling colitur repo), ONE --
+// "1962-06-23,…LT.pdf", the Archivum Liturgicum ELECTRONIC TRANSCRIPTION --
+// silently drops vigil commemorations that the other two, PHOTOGRAPHIC
+// scans of the actual 1962 Missale Romanum, both carry. Confirmed on four
+// entries: 7 August (Donatus), 9 August (Romanus), 14 August (Eusebius),
+// 25 December (Anastasia) -- all present in both photographic scans'
+// calendarium AND their own Proprium Sanctorum text ("Et fit
+// commemoratio S. Romani Mar-", "...S. Eusebii Con-", etc.), all silently
+// absent from the transcription. A `knownSpuriousComm` exclusion list once
+// stood here, built by checking ONLY the transcription and concluding two
+// of these four ("Romanus", "Eusebius" on 14 August) were spurious -- WRONG,
+// on evidence that itself was incomplete, not on a genuine absence. Treat
+// the photographic scans as the primary source and the electronic
+// transcription as a convenience index only; where they disagree, the scan
+// wins. (14 August's "St. Eusebius, Conf." and 16 December's "St. Eusebius,
+// Ep. et Mart." are two different people, both genuinely in the calendarium
+// -- see slugOverride below, not a reason to drop either.)
// slugOverride gives a proper, distinct slug to a small number of
// commemorations whose title slugifies IDENTICALLY to an unrelated feast on
@@ -323,44 +358,73 @@ var knownSpuriousComm = map[string]bool{
// feast (the same saint, repeated, not a coincidence); 14 May's "St.
// Boniface" is a different early martyr from 5 June's Boniface of Mainz, an
// entirely different person whose title happens to abbreviate to the same
-// English string. Keyed "MM-DD/original-slug" -> replacement slug.
+// English string; 14 August's "St. Eusebius" (a Confessor, calendarium "S.
+// Eusebii Conf.") is likewise a different person from 16 December's "St.
+// Eusebius" (a Bishop and Martyr, calendarium "S. Eusebii Ep. et Mart."),
+// both genuinely commemorated, missalemeum giving both the same bare
+// English title. Keyed "MM-DD/original-slug" -> replacement slug.
var slugOverride = map[string]string{
"01-28/agnes": "agnes-secundo",
+ "08-14/eusebius": "eusebius-confessor",
"05-14/boniface": "boniface-martyr",
}
-// refYearExplainsAbsence reports whether the TEMPORAL day alone (no
-// sanctoral data at all -- an empty layer stack) on this specific reference
-// date was already strong enough that ANY class-1..4 saint would lose there
-// regardless of its own merit: a Sunday or a named I/II-class feast, an
-// Ember day, the late-Advent or Christmas-octave privilege (I or II class),
-// or a privileged Lent/Passiontide feria (III class, but still privileged
-// over an equal-or-lower-class saint per RG 109(e)).
+// refYearExplainsAbsence is a RANK-BLIND SAMPLING HEURISTIC, not a rubric
+// evaluator: it does not know, and cannot know, the true class of the saint
+// it is being asked about -- only whether the TEMPORAL day alone (no
+// sanctoral data at all) on this one reference date looks privileged enough
+// that a saint failing to win there is unsurprising. It reports true for: a
+// Sunday or a named I/II-class feast, an Ember day, the late-Advent or
+// Christmas-octave privilege (I or II class), or a Lent/Passiontide feria
+// (III class, privileged per RG 109(e)).
+//
+// Two known imprecisions, recorded rather than silently accepted:
//
-// This is the discriminator between two different reasons a saint is never
-// OBSERVED in any of the six reference years:
+// - The Lent/Passiontide branch is privilege over an EQUAL-OR-LOWER-class
+// candidate only -- RG 109(e) does not let a mere III-class feria beat a
+// I- or II-class feast. Nothing in this codebase's actual data currently
+// exercises that gap (every saint this heuristic has ever been asked
+// about that is demoted throughout Lent is independently III class or
+// lower, per the calendarium), but the function does not itself enforce
+// it, so a future entry could reach it.
+// - It is a proxy for "why was this saint never observed", not a citation.
+// 6-31 December is excluded from ever trusting the id-derived rank
+// below, REGARDLESS of what this heuristic would otherwise say, because
+// RG 68(d)/(e) settles two specific cases (Thomas Becket, 29 December;
+// Silvester, 31 December) directly: the calendarium's own row names
+// each a bare "Commemoratio" with NO class of its own, even though the
+// DAY they fall on is II class (a day within the Nativity Octave). A
+// rank-blind heuristic reading the day's own class here would (and,
+// before this exclusion, did) wrongly conclude "the day was strong
+// enough to explain the demotion, so the id's rank is trustworthy" --
+// right about the day, wrong about what the rubric actually says about
+// THESE two named commemorations specifically.
//
-// 1. Genuinely without an independent Mass in the 1960-reformed books --
-// reduced to an added commemoration on ANY day, including an ordinary,
-// unprivileged one. Confirmed live: St Blaise (3 Feb, an ordinary
-// Septuagesima-season feria, non-privileged), St Canute (19 Jan, an
-// ordinary Time-after-Epiphany feria) and others are shown by
-// missalemeum as mere commemorations even then -- this is real 1962
-// data, not a sampling artefact, and RankCommemoration is the correct,
-// honest rank for them.
-// 2. A real class-1..4 feast that merely never won in these six
-// PARTICULAR reference years because its fixed date happens to fall,
-// in every one of them, on a day already strong enough to beat any
-// saint of its class -- the 15-entry, 6 March-5 April case this fix
-// originally targeted (every one of those dates falls within Lent in
-// all six reference years). Here the id's own embedded rank is
-// trustworthy.
+// The 6-31 December exclusion is also a warning about a structural hazard,
+// not just a one-off fix: this function calls calendar.Compute, i.e. it
+// reads the ENGINE's OWN computed temporal ranks to decide what DATA to
+// generate. A change to temporal_ef.go's ranking (e.g. promoting 26-31
+// December from class-4 to class-2, RG 67/68, landed in this same lineage)
+// can silently flip this function's verdict and rewrite generated data with
+// no code change to this file at all. Any future temporal_ef.go rank change
+// should re-check this function's own boundary cases, not just its own
+// tests.
//
// harvestDate keeps a saint's commemoration id rank ONLY if every reference
-// year in which it was seen was case 1 above (i.e. this function returned
-// true every time) -- a single unprivileged-day counter-example is enough
-// to fall back to RankCommemoration.
+// year in which it was seen returned true here -- a single false (an
+// unprivileged day, or the December exclusion) is enough to fall back to
+// RankCommemoration, the safe default (see St Blaise, whose own
+// commemoration id claims rank 4 and is still correctly overridden to
+// RankCommemoration, proving the id's rank is a candidate, not a verdict --
+// see the call site's own comment).
func refYearExplainsAbsence(date time.Time) bool {
+ if date.Month() == time.December && date.Day() >= 26 && date.Day() <= 31 {
+ // RG 68(d)/(e): "die 29 decembris, fit commemoratio S. Thomae
+ // Episcopi et Mart.; die 31 decembris, fit commemoratio S.
+ // Silvestri I Papae et Conf." -- both named as a bare
+ // "Commemoratio", no class. See this function's own doc comment.
+ return false
+ }
sel := calendar.DefaultSelection()
sel.Form = "old"
day := calendar.Compute(date, sel, nil)
@@ -422,7 +486,7 @@ func harvestDate(mmdd string) (*entry, []entry) {
continue // a transferred feast's commemoration, not a genuine one for THIS date
}
slug := slugify(c.Title)
- if slug == "" || knownSpuriousComm[mmdd+"/"+slug] {
+ if slug == "" {
continue
}
if t, ok := commTrack[slug]; ok {
@@ -431,14 +495,18 @@ func harvestDate(mmdd string) (*entry, []entry) {
}
continue // title/colour/id-rank already captured from the first sighting
}
- // The COMMEMORATION object's own id names THAT SAINT's true rank
- // (unlike the DAY's own info.id, which names the rank of whatever
+ // The COMMEMORATION object's own id is a BETTER rank signal than
+ // the DAY's own info.id (which names the rank of whatever
// propers are reused that day, not the commemorated saint's) --
- // idParts already extracts it; only the colour half used to be
- // kept. Whether this rank is actually TRUSTED depends on
- // `explained` across every year this slug is seen -- resolved
- // after the year loop, see refYearExplainsAbsence's own doc
- // comment.
+ // but "better" is not "always correct": St Blaise's own
+ // commemoration id is "sancti:02-03:4:r" (rank 4), and he is
+ // still, correctly, ruled RankCommemoration below, because he
+ // has no independent Mass at all in the 1960 books, not because
+ // his id's rank is wrong. The id's rank is a CANDIDATE value,
+ // trusted only when refYearExplainsAbsence's heuristic finds no
+ // counter-evidence across every reference year this slug is
+ // seen -- see that function's own doc comment for what the
+ // heuristic actually checks and its known limits.
rank, col := idParts(c.ID)
commTrack[slug] = &commTracker{
entry: entry{slug: slug, date: mmdd, colour: col, en: c.Title, rank: rank},
@@ -497,11 +565,34 @@ func harvestDate(mmdd string) (*entry, []entry) {
}
func main() {
- // Existing Latin names to preserve (missalemeum has no Latin titles).
- la := map[string]string{}
+ // preservedNames holds every "name.<lang>" field already present in the
+ // existing file, keyed by slug then by the full field name -- EVERY
+ // language missalemeum does not itself supply (it has English titles
+ // only), not a hardcoded whitelist of one or two. A whitelist is
+ // exactly the bug this replaces: an earlier version of this generator
+ // preserved only name.la, and -- unnoticed, because the bootstrapped
+ // file has in fact never carried a name.la value at all, so that
+ // mechanism was silently inert from the start -- name.pl had no
+ // preservation mechanism whatsoever. A regeneration deleted all 322
+ // Polish names outright (measured: name.pl 322 -> 0), reaching
+ // mobile.Day(date, "ef", version, "pl") -- a shipped dlectio entry
+ // point -- on the app's next build, with `naming.CelebrationName`'s own
+ // name[lang] -> name.en fallback silently substituting English and no
+ // error anywhere. Generalising to every "name.*" key except name.en
+ // (English is always freshly regenerated from missalemeum, the whole
+ // point of this tool) means a third, fourth, or Nth language added to
+ // the file later survives a regeneration without this function ever
+ // needing to change again.
+ preservedNames := map[string]map[string]string{}
for slug, rc := range caldata.Tridentine().Cels {
- if v := rc.Fields["name.la"]; v != "" {
- la[slug] = v
+ for k, v := range rc.Fields {
+ if v == "" || !strings.HasPrefix(k, "name.") || k == "name.en" {
+ continue
+ }
+ if preservedNames[slug] == nil {
+ preservedNames[slug] = map[string]string{}
+ }
+ preservedNames[slug][k] = v
}
}
@@ -580,9 +671,7 @@ func main() {
es := make([]entry, 0, len(entries))
for _, e := range entries {
- if l := la[e.slug]; l != "" {
- e.la = l
- }
+ e.otherNames = preservedNames[e.slug]
es = append(es, e)
}
sort.Slice(es, func(i, j int) bool {
@@ -599,7 +688,9 @@ func main() {
b.WriteString("; by internal/calendar (temporalEF) and is NOT listed here.\n")
b.WriteString("; Ranks use the 1960 Code of Rubrics: class-1..class-4.\n")
b.WriteString("; Generated by scripts/gen-sanctoral-ef.go from missalemeum (Divinum Officium 1962\n")
- b.WriteString("; data). Latin names are hand-curated where present. See NOTICE.\n\n")
+ b.WriteString("; data). name.en is always regenerated fresh from missalemeum; every other\n")
+ b.WriteString("; name.<lang> (missalemeum supplies English only) is preserved verbatim from\n")
+ b.WriteString("; whatever this file already carried before regeneration. See NOTICE.\n\n")
b.WriteString("[layer]\nid = tridentine\nname = General Roman Calendar of 1962\ntype = universal\n")
for _, e := range es {
fmt.Fprintf(&b, "\n[%s]\ndate = %s\nrank = %s\ncolour = %s\n", e.slug, e.date, e.rank, e.colour)
@@ -607,8 +698,13 @@ func main() {
fmt.Fprintf(&b, "class = %s\n", e.class)
}
fmt.Fprintf(&b, "name.en = %s\n", e.en)
- if e.la != "" {
- fmt.Fprintf(&b, "name.la = %s\n", e.la)
+ otherKeys := make([]string, 0, len(e.otherNames))
+ for k := range e.otherNames {
+ otherKeys = append(otherKeys, k)
+ }
+ sort.Strings(otherKeys) // deterministic output regardless of map iteration order
+ for _, k := range otherKeys {
+ fmt.Fprintf(&b, "%s = %s\n", k, e.otherNames[k])
}
if e.first != "" {
fmt.Fprintf(&b, "reading.first = %s\n", e.first)