diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/gen-sanctoral-ef.go | 264 |
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) |
