diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-14 13:22:16 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-14 13:22:16 +0200 |
| commit | 913974b10a993af251b25125d1a417c452ad785c (patch) | |
| tree | 5dd82811c100de6daa995bebd115a12434296508 /internal/caldata/caldata_test.go | |
| parent | d7da4b09f7775276231d0241cfe2700d247728ee (diff) | |
| parent | 3b32c002d3eddda5ece9422442717657b9fee63b (diff) | |
| download | lectio-913974b10a993af251b25125d1a417c452ad785c.tar.gz lectio-913974b10a993af251b25125d1a417c452ad785c.zip | |
Merge branch 'polish-ui-and-calendar': the gomobile facade and the EF calendar fixes
Two bodies of work that shared a branch.
The gomobile facade (2026-08-03..05): mobile.PartLabels, Days, and the
observed rank on DayInfo, so dlectio stops hardcoding part IDs and rank
strings; the 1962 part labels become i18n data; the documented gomobile
bind command is corrected so it reproduces the shipped .aar.
The EF calendar fixes (2026-08-12): seven defects found by differencing
this engine against colitur, a second 1962 implementation built from the
Missal's General Rubrics rather than from this codebase. RG 96 transfers
were not skipping II-class days; a II-class privileged feria was not
yielding to a feast; Sunday ranks, the two Rose Sundays and Holy Thursday's
colour were wrong; and scripts/gen-sanctoral-ef inferred ranks, deduped
and tagged classes wrongly, which put 15 III-class feasts into the shipped
tridentine-calendar.ini as bare commemorations and dropped four entries
outright.
Holy Thursday was violet in both engines, which is how a shared lineage
hides a defect: this project's ini is generated from missalemeum and
colitur's data was bootstrapped from here, so an error inherited by both
is invisible to a differential. It took the Missal itself to see it.
The EF oracle test now asserts rank and colour, not season alone. One
known gap is recorded in the source rather than fixed, as out of scope:
RG 95 chained transfers (calendar.go).
Diffstat (limited to 'internal/caldata/caldata_test.go')
| -rw-r--r-- | internal/caldata/caldata_test.go | 307 |
1 files changed, 303 insertions, 4 deletions
diff --git a/internal/caldata/caldata_test.go b/internal/caldata/caldata_test.go index 12aa765..7dbbf84 100644 --- a/internal/caldata/caldata_test.go +++ b/internal/caldata/caldata_test.go @@ -74,9 +74,13 @@ func TestTridentineLoads(t *testing.T) { t.Fatalf("EF sanctoral has only %d entries; expected the full calendar", len(l.Cels)) } // Landmark feasts must be present with the right rank/class: Sts Peter & Paul - // (I class, 06-29), the Purification/Presentation (a II class feast of the - // Lord, 02-02, which must displace a Sunday), the Immaculate Conception - // (I class, 12-08). + // (I class, 06-29), the Purification/Presentation (II class, 02-02, class + // = lord -- see classOf's own doc comment in scripts/gen-sanctoral-ef.go + // for why: the calendarium's own title names the BLESSED VIRGIN, but the + // tag exists to drive OCCURRENCE behaviour, and missalemeum -- this + // data's own oracle -- shows the Purification taking a II-class Sunday's + // place outright, the "festum Domini" pattern, not the ordinary-BVM + // pattern), the Immaculate Conception (I class, 12-08). want := map[string]struct{ date, rank, class string }{ "sts-peter-paul": {"06-29", "class-1", ""}, "purification-of-the-blessed-virgin-mary": {"02-02", "class-2", "lord"}, @@ -91,7 +95,7 @@ func TestTridentineLoads(t *testing.T) { if rc.Fields["date"] != w.date || rc.Fields["rank"] != w.rank { t.Errorf("%s: date/rank = %q/%q, want %q/%q", slug, rc.Fields["date"], rc.Fields["rank"], w.date, w.rank) } - if w.class != "" && rc.Fields["class"] != w.class { + if rc.Fields["class"] != w.class { t.Errorf("%s: class = %q, want %q", slug, rc.Fields["class"], w.class) } } @@ -103,3 +107,298 @@ func TestTridentineLoads(t *testing.T) { } } } + +// TestTridentineClassOfLord: scripts/gen-sanctoral-ef.go's classOf tags +// feasts of the Lord (class = lord). "Most Holy Name of Mary" was wrongly +// tagged lord (a title-substring false positive: "holy name" alone matches +// both "Holy Name of Jesus" and "Most Holy Name of MARY", calendarium +// "Sanctissimi Nominis Mariae") and is fixed here, not contested. The +// Baptism commemoration was missing the tag entirely (13 January's title +// ends "of THE Lord", not "of OUR Lord", the only suffix classOf used to +// check -- calendarium "IN COMMEMORATIONE BAPTISMATIS D. N. I. C."), also +// fixed and not contested. The Purification is DELIBERATELY absent from +// this table: it stays tagged lord, on occurrence-behaviour evidence, not +// the calendarium's title -- see TestTridentineLoads and classOf's own doc +// comment in scripts/gen-sanctoral-ef.go for the full account of why it is +// the one contested case, not a clean-cut fix. +func TestTridentineClassOfLord(t *testing.T) { + l := Tridentine() + want := map[string]string{ + "most-holy-name-of-mary": "", // BVM, not the Lord + "commemoration-of-the-baptism-of-the-lord": "lord", + } + for slug, wantClass := range want { + rc, ok := l.Cels[slug] + if !ok { + t.Errorf("missing landmark feast %q", slug) + continue + } + if got := rc.Fields["class"]; got != wantClass { + t.Errorf("%s: class = %q, want %q", slug, got, wantClass) + } + } +} + +// TestTridentineLentRankNotCommemoration: 15 III-class feasts, every one +// falling 6 March - 5 April, were tagged rank = commemoration because all +// six of the generator's reference years happen to place that fixed date +// within Lent, where a privileged Lenten feria always outranks an +// equal-class feast (RG 109(e)) -- the generator mistook "how the entry +// presented on the sampled days" for the entry's true, intrinsic rank. A +// sample of the 15 (full list in the report); the calendarium gives each one +// III class outright, e.g. 5 April "S. Vincentii Ferrerii Conf., III +// classis." +func TestTridentineLentRankNotCommemoration(t *testing.T) { + l := Tridentine() + for _, slug := range []string{ + "sts-felicitas-perpetua", "thomas-aquinas", "frances-rome", + "gregory-the-great", "patrick", "benedict", "francis-of-paola", + "isidore-of-seville", "vincent-ferrer", + } { + rc, ok := l.Cels[slug] + if !ok { + t.Errorf("missing %q", slug) + continue + } + if got := rc.Fields["rank"]; got != "class-3" { + t.Errorf("%s: rank = %q, want class-3 (not commemoration)", slug, got) + } + } +} + +// TestTridentineGenuineCommemorationStaysCommemoration: the fix above must +// NOT promote every commemoration-only entry indiscriminately -- some really +// are without an independent Mass in the 1960-reformed books, demoted even +// on an ORDINARY, unprivileged day (not explained by Lent/Passiontide or any +// higher-class temporal day). St Blaise (3 Feb, an ordinary Septuagesima- +// season feria) is live-confirmed by missalemeum as a mere commemoration +// even then, so RankCommemoration is the honest, correct rank -- promoting +// him to class-4 would let him wrongly win an ordinary green-season feria he +// has no independent Mass to celebrate. +func TestTridentineGenuineCommemorationStaysCommemoration(t *testing.T) { + l := Tridentine() + rc, ok := l.Cels["blaise"] + if !ok { + t.Fatal("missing blaise") + } + if got := rc.Fields["rank"]; got != "commemoration" { + t.Errorf("blaise: rank = %q, want commemoration (genuinely no independent Mass)", got) + } +} + +// TestTridentineNoMissingEntries: four entries present in missalemeum (and +// so in the calendarium) were silently dropped by the generator's own +// dedup, which keyed a single global map by SLUG alone -- two by a literal +// slug collision with an unrelated feast of the same English title on a +// different date (28 January's "St. Agnes", the traditional SECOND +// commemoration of 21 January's own Agnes; 14 May's "St. Boniface", a +// different martyr from 5 June's Boniface of Mainz), and two (Evaristus, +// Theodore) by the generator returning on the first reference year that +// showed an observed office, before ever reaching the later year that +// revealed their commemoration. +func TestTridentineNoMissingEntries(t *testing.T) { + l := Tridentine() + want := map[string]string{ + "agnes-secundo": "01-28", + "boniface-martyr": "05-14", + "evaristus": "10-26", + "theodore": "11-09", + } + for slug, date := range want { + rc, ok := l.Cels[slug] + if !ok { + t.Errorf("missing %q (%s)", slug, date) + continue + } + if got := rc.Fields["date"]; got != date { + t.Errorf("%s: date = %q, want %q", slug, got, date) + } + } +} + +// TestTridentineRomanusAndEusebiusPresent: CORRECTED after review found the +// opposite claim resting on an incomplete primary-source check. An earlier +// version of this test (and of scripts/gen-sanctoral-ef.go's +// knownSpuriousComm) asserted Romanus ABSENT, on the strength of ONE of the +// three local Missal scans -- "1962-06-23,…LT.pdf", an ELECTRONIC +// TRANSCRIPTION that silently drops vigil commemorations generally (also +// missing: 7 August Donatus, 25 December Anastasia, both present elsewhere). +// The other two, PHOTOGRAPHIC scans of the actual 1962 Missale Romanum, both +// carry it: "missale-romanum-1962.pdf" calendarium, 9 August row: "XVI d V +// 9 Vigilia, III classis, Commemoratio S. Romani Mart.", with the saint's +// own proper text elsewhere in the same scan ("Et fit commemoratio S. Romani +// Mar-"), and its own back-of-book index ("Romani Mart., 9 augusti ... 621"). +// Where the scans and the transcription disagree, the scans win -- see +// gen-sanctoral-ef.go's own primary-source note, same rule recorded there so +// it is not lost a second time. +// +// 14 August's "St. Eusebius" is the identical shape (calendarium: "XI b XIX +// 14 Vigilia, II classis, Commemoratio S. Eusebii Conf.") and a DIFFERENT +// person from 16 December's "St. Eusebius, Ep. et Mart." (calendarium: "V +// XVII S. Eusebii Ep. et Mart., III classis.") -- a Confessor and a Bishop +// and Martyr, not the same saint moved or duplicated. missalemeum gives both +// the same bare English title, so they collide by slug; slugOverride +// disambiguates rather than either being dropped. +func TestTridentineRomanusAndEusebiusPresent(t *testing.T) { + l := Tridentine() + if rc, ok := l.Cels["romanus"]; !ok { + t.Error("romanus missing: the calendarium's photographic scans both carry \"Commemoratio S. Romani Mart.\" on 9 August") + } else if rc.Fields["date"] != "08-09" { + t.Errorf("romanus: date = %q, want 08-09", rc.Fields["date"]) + } + want := map[string]struct{ date, rank string }{ + // St Eusebius Confessor (14 Aug) and St Romanus (9 Aug, checked + // above) are both bare "Commemoratio" in the calendarium (no class + // of their own) -- pinned here, not just presence/date, per + // TestTridentineCommemorationRanksPinned's own reasoning below: + // romanus's own rank is pinned there instead, since that test + // groups every commemoration-rank entry this task's fixes touched + // in one place. eusebius-confessor is listed here because it is + // the OTHER half of this specific test's own slug-disambiguation + // story. + "eusebius-confessor": {"08-14", "commemoration"}, // "S. Eusebii Conf." -- a Confessor + "eusebius": {"12-16", "class-3"}, // "S. Eusebii Ep. et Mart." -- a Bishop and Martyr, a different person, unaffected by this round + } + for slug, w := range want { + rc, ok := l.Cels[slug] + if !ok { + t.Errorf("missing %q (%s)", slug, w.date) + continue + } + if rc.Fields["date"] != w.date { + t.Errorf("%s: date = %q, want %q", slug, rc.Fields["date"], w.date) + } + if rc.Fields["rank"] != w.rank { + t.Errorf("%s: rank = %q, want %q", slug, rc.Fields["rank"], w.rank) + } + } +} + +// TestTridentineCommemorationRanksPinned: nothing in this test suite +// asserted `rank` for thomas-becket, silvester, or romanus before this -- +// `TestTridentineRomanusAndEusebiusPresent` above pinned presence and date +// for romanus but not rank, and Thomas Becket/Silvester (the I3 fix, RG +// 68(d)/(e)) had no rank assertion anywhere at all. So a future +// regeneration could silently rewrite any of the three back to class-4 -- +// the EXACT I3 failure mode -- with the suite green throughout. Pinned +// together because they are the same failure shape (a bare "Commemoratio" +// in the calendarium, no class of its own, silently promoted by +// refYearExplainsAbsence's own coupling to temporal_ef.go, see that +// function's doc comment) even though they were found in two different +// review rounds (romanus in C2, Thomas Becket/Silvester in I3). +func TestTridentineCommemorationRanksPinned(t *testing.T) { + l := Tridentine() + for _, slug := range []string{"romanus", "thomas-becket", "silvester"} { + rc, ok := l.Cels[slug] + if !ok { + t.Errorf("missing %q", slug) + continue + } + if rc.Fields["rank"] != "commemoration" { + t.Errorf("%s: rank = %q, want commemoration", slug, rc.Fields["rank"]) + } + } +} + +// TestTridentineNoTransferArtifacts: a movable-transfer feast displayed on +// whatever civil date it actually landed on in a given reference year (St +// Joseph pushed to 20 March by a Sunday of Lent; the Annunciation deferred +// past Holy Week; All Souls moved to the Monday; St Matthias shown on the +// 25th in a leap year) must not leak into the sanctoral as a phantom +// fixed-date entry keyed to that transferred civil date. +func TestTridentineNoTransferArtifacts(t *testing.T) { + l := Tridentine() + for _, slug := range []string{ + "joseph-spouse-of-the-bl-virgin-mary-0320", + "annunciation-of-the-blessed-virgin-mary-0405", + "annunciation-of-the-blessed-virgin-mary-0408", + "annunciation-of-the-blessed-virgin-mary-0409", + "commemoration-of-all-souls-1103", + "matthias-0225", + } { + if _, ok := l.Cels[slug]; ok { + t.Errorf("phantom transfer-artifact entry %q present", slug) + } + } +} + +// TestTridentineNamesPreservedAcrossRegeneration is the coverage guard a +// regeneration silently destroying a whole language's names needed and did +// not have: an earlier version of scripts/gen-sanctoral-ef.go's main() +// preserved existing name.la values across a regeneration (missalemeum +// supplies English only) but had no equivalent for name.pl -- and, because +// the bootstrapped file has in fact never carried a name.la value, that +// mechanism looked correct while doing nothing at all. A regeneration +// deleted all 322 Polish names outright (name.pl count 322 -> 0), silently: +// no test here asserted anything about a name.* field, and +// `len(l.Cels) < 250` (TestTridentineLoads) does not notice a field going +// missing within entries that still exist. `naming.CelebrationName`'s own +// name[lang] -> name.en fallback (internal/naming/naming.go) then quietly +// substituted English for Polish on every EF display in that language, +// reaching mobile.Day(date, "ef", version, "pl") -- a shipped dlectio entry +// point -- with no error anywhere in the chain. +// +// CORRECTED after review: the first version of this test counted name.pl +// only (a hardcoded single language -- the exact "whitelist of two +// languages" mistake C1's own fix was written to stop repeating, just +// moved into the test), and its coverage floor (>= 315) had 7 entries of +// slack -- the review proved dropping 6 entries' Polish names still passed +// it. Fixed on both axes: the language set is DISCOVERED from the data +// (every "name.<lang>" key actually present, not a hardcoded list, so a +// regeneration dropping name.la or introducing a future name.de is +// checked the same way as name.pl), and the floor for each discovered +// language is its EXACT true count, not a loose approximation -- verified +// directly against the branch point (`git show 2386a45:...`) once, by +// hand, and hardcoded as the answer, not derived at test time from data +// that could itself be wrong. +// +// True counts, independently verified: name.en on every entry (327, all +// regenerated fresh from missalemeum, including the 5 entries this task's +// own fixes added); name.pl on exactly 322 (preserved from the branch +// point; the 5 new entries -- St Agnes secundo, St Boniface Martyr, St +// Evaristus, St Theodore, St Eusebius Confessor -- never had a curated +// Polish name to preserve in the first place, so 322, not 327, is the +// correct target, not a shortfall); no OTHER name.<lang> exists in the +// branch point at all (name.la is read by the generator but the +// bootstrapped file has in fact never carried one), so the discovered +// language set itself must be exactly {en, pl} -- a regeneration that +// silently introduced or lost an entire language key, not just some +// values within one, is caught by this assertion, not only by the count. +func TestTridentineNamesPreservedAcrossRegeneration(t *testing.T) { + l := Tridentine() + counts := map[string]int{} + for _, rc := range l.Cels { + for k, v := range rc.Fields { + if v == "" || !strings.HasPrefix(k, "name.") { + continue + } + lang := strings.TrimPrefix(k, "name.") + counts[lang]++ + } + } + wantLangs := map[string]bool{"en": true, "pl": true} + for lang := range counts { + if !wantLangs[lang] { + t.Errorf("unexpected name.%s present (%d entries) -- discovered language set must be exactly {en, pl}", lang, counts[lang]) + } + } + for lang := range wantLangs { + if counts[lang] == 0 { + t.Errorf("name.%s entirely absent -- discovered language set must be exactly {en, pl}", lang) + } + } + if got := counts["en"]; got != len(l.Cels) { + t.Errorf("name.en coverage = %d entries, want %d (every entry, all freshly regenerated)", got, len(l.Cels)) + } + if got := counts["pl"]; got != 322 { + t.Errorf("name.pl coverage = %d entries, want exactly 322 (the branch point's own true count; the 5 entries this task added have no curated Polish name to preserve, so more or fewer than 322 is a bug either way)", got) + } + + rc, ok := l.Cels["assumption-of-the-blessed-virgin-mary"] + if !ok { + t.Fatal("missing assumption-of-the-blessed-virgin-mary") + } + if got := rc.Fields["name.pl"]; got != "Wniebowzięcie N. M. P." { + t.Errorf("assumption-of-the-blessed-virgin-mary: name.pl = %q, want the preserved Polish name", got) + } +} |
