aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 14:58:20 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-18 14:58:20 +0200
commit46653b24b0afff60be35c3e876830eb522fd2acc (patch)
treeb86e6aa95937b69a5b3bdbf882c62775da25271b
parent118dacf366d7864bc8e9ecb8f7b6a70d1b9fdb84 (diff)
downloadlectio-46653b24b0afff60be35c3e876830eb522fd2acc.tar.gz
lectio-46653b24b0afff60be35c3e876830eb522fd2acc.zip
feat(gen): teach the EF generator to preserve the Missal corrections
The four calendar corrections adopted from colitur are not in missalemeum, so re-running gen-sanctoral-ef.go silently reverted them. Warning about that in three places was the stopgap; this fixes it. The generator now carries a missalOverrides table -- slug, field, required value, and the authority for it -- applied after every harvest and collision fix-up has settled, so the Missal's value is the last word. Regenerating is now safe; hand-editing the generated file is not, and both the file's header and the generator's comment now say which way round that is. An explicit table rather than "preserve whatever the existing file had", which is how the name.<lang> preservation beside it works and would have been less code. Preserving from the file cannot distinguish a deliberate correction from staleness: if missalemeum ever fixes one of these, a preserve-from-file rule would keep overriding it forever with nobody the wiser. Naming each override with its rubric makes an obsolete one findable by reading four lines. The apply step reports "already correct upstream" when it finds one, which is the signal to retire it. It FAILS rather than warns when an override names a slug the run did not harvest. A rename upstream would otherwise drop a correction silently, which is the precise accident the table exists to prevent. TestTridentineMissalOverrides pins all four values from the other side, so a regeneration that stopped applying them fails the suite rather than only failing quietly. Mutation-tested: reverting the Assumption vigil to missalemeum's white reddens it with a message naming the cause. It also asserts the Ascension's vigil is NOT violet -- RG 128's rule is "outside Paschaltide" and that one is inside it -- so a careless "make every vigil violet" fix fails too.
-rw-r--r--internal/caldata/caldata_test.go54
-rw-r--r--internal/caldata/tridentine-calendar.ini16
-rw-r--r--scripts/gen-sanctoral-ef.go107
3 files changed, 166 insertions, 11 deletions
diff --git a/internal/caldata/caldata_test.go b/internal/caldata/caldata_test.go
index 7dbbf84..c0e2d50 100644
--- a/internal/caldata/caldata_test.go
+++ b/internal/caldata/caldata_test.go
@@ -402,3 +402,57 @@ func TestTridentineNamesPreservedAcrossRegeneration(t *testing.T) {
t.Errorf("assumption-of-the-blessed-virgin-mary: name.pl = %q, want the preserved Polish name", got)
}
}
+
+// TestTridentineMissalOverrides pins the four fields where the 1962 MISSALE
+// ROMANUM decides against missalemeum, and the Missal wins.
+//
+// This is the safety net for a specific accident. This file is generated by
+// scripts/gen-sanctoral-ef.go from missalemeum, which does NOT carry any of
+// these values; the generator re-applies them from its own missalOverrides
+// table on every run. If that table is ever removed, or a slug is renamed
+// upstream so an override silently stops matching, the generated file quietly
+// reverts to missalemeum's values and nothing else in this repository would
+// notice. This test notices.
+//
+// Each was found by the sibling project colitur, which validates this data
+// against the Missal and its Rubricae Generales rather than against another
+// calendar program:
+//
+// - ubaldus and didacus: the Missal's own universal calendarium ranks both
+// "III classis" outright, with no commemoration rubric, and each has a
+// Mass entry of its own in the Proprium taking its readings from a Common
+// -- which a bare commemoration never has.
+// - the two August vigils: RG 128 gives violet to "vigils of II and III
+// class outside Paschaltide". Both qualify. The rule's sole exception is
+// the Ascension's vigil, inside Paschaltide, which is white -- and which
+// is therefore checked here too, in the opposite direction, so that a
+// careless "make every vigil violet" fix would fail rather than pass.
+func TestTridentineMissalOverrides(t *testing.T) {
+ l := Tridentine()
+ for _, c := range []struct{ slug, field, want, why string }{
+ {"ubaldus", "rank", "class-3", "Missal calendarium: S. Ubaldi Ep. et Conf., III classis"},
+ {"didacus", "rank", "class-3", "Missal calendarium: S. Didaci Conf., III classis"},
+ {"vigil-of-st-lawrence", "colour", "violet", "RG 128: III-class vigil outside Paschaltide"},
+ {"vigil-of-the-assumption", "colour", "violet", "RG 128: II-class vigil outside Paschaltide"},
+ } {
+ rc, ok := l.Cels[c.slug]
+ if !ok {
+ t.Errorf("missing %q -- if the slug was renamed upstream, the generator's "+
+ "missalOverrides entry must be renamed with it, or the correction is lost", c.slug)
+ continue
+ }
+ if got := rc.Fields[c.field]; got != c.want {
+ t.Errorf("%s: %s = %q, want %q (%s). This is a MISSAL correction that "+
+ "missalemeum does not carry -- if it reverted, the generator's "+
+ "missalOverrides table has stopped being applied", c.slug, c.field, got, c.want, c.why)
+ }
+ }
+ // The exception, asserted in the other direction: RG 128's vigil rule is
+ // "outside Paschaltide", and the Ascension's vigil is inside it.
+ if rc, ok := l.Cels["vigil-of-the-ascension"]; ok {
+ if got := rc.Fields["colour"]; got != "" && got != "white" {
+ t.Errorf("vigil-of-the-ascension: colour = %q, want white -- RG 128's "+
+ "violet rule is for vigils OUTSIDE Paschaltide and this one is inside it", got)
+ }
+ }
+}
diff --git a/internal/caldata/tridentine-calendar.ini b/internal/caldata/tridentine-calendar.ini
index 2786125..51b931a 100644
--- a/internal/caldata/tridentine-calendar.ini
+++ b/internal/caldata/tridentine-calendar.ini
@@ -8,12 +8,16 @@
; name.<lang> (missalemeum supplies English only) is preserved verbatim from
; whatever this file already carried before regeneration. See NOTICE.
;
-; NO LONGER PURELY GENERATED -- READ BEFORE RE-RUNNING THE GENERATOR.
-; Four fields below are HAND CORRECTIONS against the 1962 Missale Romanum
-; itself, adopted 2026-08-18 from the sibling project colitur, which validated
-; this data against the Missal and found missalemeum wrong on each. They are
-; NOT in missalemeum and a regeneration WILL SILENTLY REVERT THEM. Re-apply
-; them, or teach the generator to, before committing regenerated output:
+; FOUR FIELDS ARE NOT FROM missalemeum. They are corrections against the 1962
+; Missale Romanum itself, adopted 2026-08-18 from the sibling project colitur,
+; which validated this data against the Missal and found missalemeum wrong on
+; each. missalemeum does not carry them.
+;
+; REGENERATING THIS FILE IS SAFE. gen-sanctoral-ef.go re-applies all four from
+; its own missalOverrides table on every run, and fails loudly rather than
+; silently dropping one whose slug has been renamed upstream.
+; EDITING THEM HERE BY HAND IS NOT SAFE -- the next run puts them back. Change
+; the table. TestTridentineMissalOverrides pins all four either way:
;
; [ubaldus] rank commemoration -> class-3
; [didacus] rank commemoration -> class-3
diff --git a/scripts/gen-sanctoral-ef.go b/scripts/gen-sanctoral-ef.go
index 34bd184..ba4d9ce 100644
--- a/scripts/gen-sanctoral-ef.go
+++ b/scripts/gen-sanctoral-ef.go
@@ -11,10 +11,12 @@
// [vigil-of-st-lawrence] colour red -> violet
// [vigil-of-the-assumption] colour white -> violet
//
-// missalemeum does not carry them, so RUNNING THIS GENERATOR WILL SILENTLY
-// REVERT ALL FOUR. Re-apply them before committing regenerated output, or
-// teach this program to. The ini file's own header repeats this list and the
-// rubrics behind it, so neither place can rot without the other showing it.
+// missalemeum does not carry them. This generator RE-APPLIES all four on every
+// run, from the missalOverrides table below, and fails rather than warns if
+// one names a slug this run did not harvest -- so a rename upstream stops the
+// build instead of quietly dropping a correction. Change the table, not the
+// generated file. internal/caldata's TestTridentineMissalOverrides pins the
+// same four values from the other side, so neither can rot alone.
//
// gen-sanctoral-ef generates the EF (Extraordinary Form, 1962) universal
// sanctoral (internal/caldata/tridentine-calendar.ini) from missalemeum's
@@ -292,6 +294,80 @@ func fetch(date string) (*mmDay, error) {
return nil, err
}
+// missalOverride is a field where the 1962 MISSALE ROMANUM ITSELF decides
+// against missalemeum, and the Missal wins. Applied after harvesting, just
+// before the file is written.
+//
+// Why an explicit table rather than "preserve whatever the existing file had"
+// -- which is how otherNames below works, and would have been less code.
+// Preserving from the file cannot tell a deliberate correction from simple
+// staleness: if missalemeum ever fixes one of these, a preserve-from-file rule
+// would keep overriding it forever and nobody would notice. Naming each
+// override with its authority says what we mean, and makes an obsolete one
+// findable by re-reading four lines rather than by diffing a calendar.
+//
+// Each was found by the sibling project colitur, which validates this data
+// against the Missal and its Rubricae Generales rather than against another
+// calendar program. See its docs/research/rules-register.md sections 6.24 and
+// 6.25 for the full account, and internal/calendar/oracle_ef_test.go for the
+// allow-list entries these four require, six days of its 730-day window.
+type missalOverride struct {
+ slug string
+ field string // "rank" or "colour"
+ want string
+ why string
+}
+
+var missalOverrides = []missalOverride{
+ {"ubaldus", "rank", "class-3",
+ `Missal calendarium: "S. Ubaldi Ep. et Conf., III classis" (16 May), no ` +
+ `commemoration rubric, and a Mass entry of its own in the Proprium ` +
+ `taking its readings from a Common -- which a bare commemoration never has`},
+ {"didacus", "rank", "class-3",
+ `Missal calendarium: "S. Didaci Conf., III classis" (13 Nov), same shape ` +
+ `as ubaldus above`},
+ {"vigil-of-st-lawrence", "colour", "violet",
+ `RG 128: violet for vigils of II and III class outside Paschaltide. ` +
+ `9 August is a III-class vigil outside Paschaltide`},
+ {"vigil-of-the-assumption", "colour", "violet",
+ `RG 128, same rule; 14 August is a II-class vigil outside Paschaltide. ` +
+ `The rule's sole exception is the Ascension's vigil, INSIDE Paschaltide, ` +
+ `which is white and is deliberately not listed here`},
+}
+
+// applyMissalOverrides rewrites the harvested entries in place and reports
+// what it did. It FAILS rather than warns when an override names a slug that
+// is not present: a rename upstream would otherwise drop a correction
+// silently, which is the exact failure this table exists to prevent.
+func applyMissalOverrides(entries map[string]entry) error {
+ for _, o := range missalOverrides {
+ e, ok := entries[o.slug]
+ if !ok {
+ return fmt.Errorf("missal override names [%s], which this run did not "+
+ "harvest -- has the slug changed upstream? Fix the override or the "+
+ "generator; do NOT delete it silently. Reason on record: %s", o.slug, o.why)
+ }
+ var had string
+ switch o.field {
+ case "rank":
+ had, e.rank = string(e.rank), calendar.Rank(o.want)
+ case "colour":
+ had, e.colour = e.colour, o.want
+ default:
+ return fmt.Errorf("missal override for [%s]: unknown field %q", o.slug, o.field)
+ }
+ entries[o.slug] = e
+ if had == o.want {
+ fmt.Fprintf(os.Stderr, "\nmissal override [%s] %s: already %s upstream -- "+
+ "missalemeum may have fixed this; consider retiring the override\n",
+ o.slug, o.field, o.want)
+ } else {
+ fmt.Fprintf(os.Stderr, "\nmissal override [%s] %s: %s -> %s\n", o.slug, o.field, had, o.want)
+ }
+ }
+ return nil
+}
+
type entry struct {
slug, date, colour, class, en, first, gospel string
rank calendar.Rank
@@ -742,6 +818,16 @@ func main() {
}
}
+ // The Missal overrides missalemeum on four fields -- see missalOverrides
+ // above for each one's authority. Applied HERE, after every harvest and
+ // collision fix-up has settled and before anything is written, so that the
+ // value the Missal requires is the last word rather than something a later
+ // pass could overwrite.
+ if err := applyMissalOverrides(entries); err != nil {
+ fmt.Fprintf(os.Stderr, "\n%v\n", err)
+ os.Exit(1)
+ }
+
es := make([]entry, 0, len(entries))
for _, e := range entries {
e.otherNames = preservedNames[e.slug]
@@ -763,7 +849,18 @@ func main() {
b.WriteString("; Generated by scripts/gen-sanctoral-ef.go from missalemeum (Divinum Officium 1962\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("; whatever this file already carried before regeneration. See NOTICE.\n")
+ b.WriteString(";\n")
+ b.WriteString("; FOUR FIELDS ARE NOT FROM missalemeum. They are corrections against the 1962\n")
+ b.WriteString("; Missale Romanum itself, which missalemeum does not carry, re-applied on every\n")
+ b.WriteString("; run by this generator's own missalOverrides table (see its comment for the\n")
+ b.WriteString("; authority behind each). Regenerating this file is therefore SAFE and will\n")
+ b.WriteString("; not revert them; editing them here by hand is not, because the next run\n")
+ b.WriteString("; puts them back. Change the table instead.\n")
+ for _, o := range missalOverrides {
+ fmt.Fprintf(&b, "; [%s] %s = %s\n", o.slug, o.field, o.want)
+ }
+ b.WriteString("\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)