From b5dcdd8d4048295cfbdf1606f4cafd39b3c433e6 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 10:40:40 +0200 Subject: temporal(ef): Holy Name of Jesus, RG 17(a) -- fallback + Lord subject RG 17(a): "festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- both photographic scans, confirmed on the calendarium's own January table and the Mass propers' own heading. Two defects closed together, since they interact through RG 91 entry 14/RG 16(a): 1. The Sunday shape (2-5 January) now tags subject = Lord, matching RG 91 entry 14 ("Festa Domini II classis") -- previously subject = Temporal, the same defect shape Holy Family had before it. 2. The fallback (RG 17(a)'s own "secus die 2 ianuarii", the calendarium's "vel, ea deficiente, die 2 ianuarii") is now built: 2 January carries the feast whenever no Sunday falls 2-5 January that year. Before this fix colitur emitted no Holy Name office at all in such a year -- a real II-class feast simply missing, on 3,619 of 8,417 years in the domain (43%; 20 of the 46 years 2005-2050, independently re-derived against `date -d`). Both shapes band identically at RG 91 entry 14's MOVABLE half (Precedence_ef.entry_14_movable_band) -- one feast, not two differently classified ones, per the Mass propers' own single heading covering both dates. No live collision exists in the current sanctoral data (nothing fixed-date falls 2-5 January besides a Commemoration_only entry on 5 Jan), so the RG 16(a)/RG 112(a) interaction is proved with synthetic precedence-level fixtures mirroring Holy Family's own precedent, not a live-data regression. One asymmetry the synthetic tests found and kept honest rather than forced: a losing Holy-Name-Sunday is privileged (RG 109(a), "of a Sunday") and survives RG 111(a)'s cap; a losing Holy-Name-fallback is not (2 January is not a Sunday, and no other RG 109(a)-(f) category applies to it), so it gets no commemoration at all if ever outranked -- both individually correct readings of RG 109/111's closed lists. Golden pins follow the brief's own vacuity warning: the Sunday shape already emitted the right slug/rank/colour before this fix (an ordinary Sunday and Holy Name coincide on those fields), so its pin asserts subject explicitly rather than pinning the whole line uncritically; the fallback pin (2029-01-02) is the one with teeth against the larger defect, reddening on slug/rank/subject together if reverted. Mutation-tested: each half of the fix reverted separately reddens exactly its own tests (Sunday subject: the dedicated unit test, the golden pin's subject field, one precedence end-to-end test; fallback: the fallback unit/anchor/golden tests, the new C16 differential count, and both shapes of the precedence end-to-end tests) and nothing else. Adds data/ef/expected-divergences.sexp's C16 (lectio has no fallback for this window at all) -- the lectio differential's own count, independently re-derived against `date -d` for the fixture's 2005-2050 span, not transcribed from the domain-wide figure. The missalemeum oracle window (2026-2027) exercises only the Sunday shape and needed no new entry: subject is not one of its compared fields, and both years' rows already corroborate the Sunday-shape ranking (missalemeum's own 2026-01-04 and 2027-01-03 rows: rank 2, white, title "Holy Name of Jesus"). --- lib/rites/rite_ef/temporal_ef.ml | 122 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) (limited to 'lib/rites/rite_ef/temporal_ef.ml') diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml index 1ce9de9..721790d 100644 --- a/lib/rites/rite_ef/temporal_ef.ml +++ b/lib/rites/rite_ef/temporal_ef.ml @@ -104,6 +104,56 @@ let christ_the_king y = sunday_on_or_before (mk y 10 31) date against it without reaching into [week_origin]'s implementation. *) let holy_family_sunday y = Date.add_days (sunday_on_or_before (mk y 1 6)) 7 +(* RG 17(a) (Caput III, "De Dominicis"; full text on [holy_family_sunday]'s + own citation above): "festum Ss.mi Nominis Iesu, celebrandum dominica quae + occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- the feast of the + Most Holy Name of Jesus, celebrated on the Sunday falling 2-5 January, + OTHERWISE [on] 2 January [itself]. [None] here means no such Sunday exists + that year -- [holy_name_fallback_date]'s own fallback branch is for + exactly that case; see its own citation for why the window CAN be empty + (unlike (b)'s 7-13 January window, which never is). + + Formula: the Sunday on or before 5 January is in [2,5] iff it is not + before 2 January -- the window is only 4 days wide, so there is at most + one Sunday in it either way. *) +let holy_name_sunday y = + let s = sunday_on_or_before (mk y 1 5) in + if on_or_after s (mk y 1 2) then Some s else None + +(* RG 17(a)'s own "secus die 2 ianuarii" clause, and the calendarium's + January table, BOTH photographic scans, word for word (docs/research/ + rules-register.md; also confirmed in the Mass propers' own heading for + this feast, both scans, Caput "Sanctissimi Nominis Iesu": "DOMINICA a die + 2 ad diem 5 ianuarii occurrenti, VEL, SI HAEC DEFECERIT, DIE 2 IANUARII" -- + the SUNDAY occurring 2-5 January, OR, IF THIS IS LACKING, [on] 2 January): + *"Dominica inter octavam Nativitatis Domini et Epiphaniam, vel, EA + DEFICIENTE, die 2 ianuarii: Sanctissimi Nominis Iesu, II classis"* -- the + Sunday between the Octave of the Nativity and Epiphany, or, THAT FAILING, + 2 January. Unlike [holy_family_sunday]'s 7-13 January window (which can + never be empty of a Sunday), this 2-5 January window CAN be, and is in + 3,619 of the 8,417 years in [1583, 9999] (a plain weekday check, + independently cross-checked against `date -d`) -- [temporal]'s own + dispatch applies this fallback date, tagged exactly like the Sunday shape + (subject Lord, the same Latin name), only when [holy_name_sunday y] is + [None] for that date's own civil year. *) +let holy_name_fallback_date y = mk y 1 2 + +(* The ONE Latin title both of RG 17(a)'s two shapes (the Sunday and the 2 + January fallback) carry -- ONE feast, RG 91's own "primum mobilia, deinde + fixa" language notwithstanding (see [temporal]'s own fallback-branch + comment for why both shapes band identically at entry 14's MOVABLE half, + not split across the two halves by year). Bound once so the two [temporal] + branches that use it cannot silently drift apart, the same discipline + [holy_family_sunday] already gets from being named instead of re-derived. + Both photographic scans, word for word, twice over (the calendarium's own + January table AND the Mass propers' own running header/heading, this + file's own citations on [holy_name_sunday]/[holy_name_fallback_date] + above): "Sanctissimi Nominis Iesu". No English name, deliberately -- the + SAME "would read the oracle it is compared against" reasoning + [holy_family_sunday]'s own [names] comment below gives for Holy Family. *) +let holy_name_names = + Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctissimi Nominis Iesu") ] + let same a b = Date.compare a b = 0 (* Named temporal days: the I-class feasts of the Lord (RG 91 entries 1 and 3), @@ -533,15 +583,68 @@ let temporal d = item on Holy Name of Jesus, RG 17(a)) -- this is a targeted addition for the one day this task built, not a claim that the gap is closed generally. *) + (* RG 17(a) (this file's own [holy_name_sunday], full citation + there): the SECOND Sunday a year this branch must not + leave [subject = Temporal] -- added alongside Holy + Family's own check above, not replacing it (the two + windows, 2-5 and 7-13 January, never overlap, so at most + one of the two conditions is ever true for a given [d]). + Guarded on [holy_name_sunday y = Some d] rather than only + "is [d] in [2,5] January": every OTHER Sunday in that + range must stay unnamed, and in a year where a DIFFERENT + day of the window is the one true Sunday, [d] itself is + never reached by [sunday_slug] as a Sunday to begin with + (weekday alone already excludes it) -- but writing the + check this way, against the SAME independently-computed + date [holy_name_sunday] returns rather than a bare month/ + day range, is what test_temporal_ef.ml's own anchor test + below cross-checks against. *) let subject, names = if same d (holy_family_sunday y) then ( Colitur_kernel.Subject.Lord, Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctae Familiae Iesu, Mariae, Ioseph") ] ) + else if holy_name_sunday y = Some d then (Colitur_kernel.Subject.Lord, holy_name_names) else (Colitur_kernel.Subject.Temporal, Colitur_kernel.Names.empty) in build ~subject ~names ~season:s ~slug ~colour ~rank ~week:(week d) () - | None -> ( + | None -> + (* RG 17(a)'s own fallback, "secus die 2 ianuarii" (full + citation on [holy_name_fallback_date] above): ONLY when + [holy_name_sunday y] is [None] for [d]'s own civil year -- + i.e. 2-5 January genuinely has no Sunday that year -- does + 2 January itself carry the feast; every other year, 2 + January is an ordinary Christmastide feria, exactly what + [christmastide_feria_slug] below already computes for it. + Checked here, ahead of that generic fallback, the same way + [named]'s own fixed dates are checked ahead of everything + ferial -- NOT folded into [named] itself, because unlike + every one of [named]'s ~20 entries this one is + CONDITIONAL on a per-year fact ([holy_name_sunday y] = + [None]) that [named]'s bare [Date.t -> ... option] shape + has no way to express without threading [subject] through + every one of its other branches too -- the same reason + Holy Family, immediately above, is not one of [named]'s + outputs either (that branch's own comment). + + Season Christmastide (RG 72-73: 2 January is always within + it); rank/colour/subject/names identical to the Sunday + shape immediately above -- ONE feast, {!holy_name_names}'s + own comment on why both shapes share it. Band-classified + at entry 14's MOVABLE half regardless of which shape fired + this year (Precedence_ef.band; both shapes are built + through this same [temporal] function, hence [origin = + Temporal] either way) -- RG 91's "primum mobilia, deinde + fixa" split is a property of the TABLE ENTRY a feast + occupies, and RG 17(a) names ONE feast with a fallback + clause, not two differently-classified feasts that happen + to share a Mass formulary; the Mass propers' own heading + (docs/research/rules-register.md) states both shapes under + the identical title for exactly this reason. *) + if same d (holy_name_fallback_date y) && holy_name_sunday y = None then + build ~subject:Colitur_kernel.Subject.Lord ~names:holy_name_names ~season:Christmastide + ~slug:"ef-holy-name" ~colour:Colour.White ~rank:Class2 ~week:(week d) () + else ( match christmastide_feria_slug d with | Some slug -> build ~season:s ~slug ~colour:(season_colour s) ~rank:(ferial_rank d s) @@ -608,7 +711,19 @@ let temporal d = 4-tuple), but it is exactly the same kind of independently-computed anchor this list exists to guard -- an accidental drift in [holy_family_sunday]'s own [+7] would silently move Holy Family without - this guard catching it, same as any other entry here. *) + this guard catching it, same as any other entry here. + + Holy Name of Jesus (RG 17(a)) is the SAME shape, restated per-year rather + than as a single fixed pair: exactly ONE of its two slugs exists for any + given [y] ([holy_name_sunday y] is [Some] xor [None]), so exactly one + entry -- "ef-holy-name-sunday" at that Sunday, or "ef-holy-name" at + [holy_name_fallback_date y] -- is appended below, matching whichever + shape [temporal] itself will actually build that year. An erosion of + EITHER branch is still caught: {!Validate.run} calls [anchors] for both + the civil year a date falls in and the one before it (a liturgical year + straddles two), so across any two consecutive years both shapes are + exercised regardless of which one civil year [y] itself happens to + land on. *) let anchors y = let easter = Computus.gregorian_easter y in let off n = Date.add_days easter n in @@ -633,6 +748,9 @@ let anchors y = ("ef-corpus-christi", off 60); ("ef-sacred-heart", off 68); ("ef-christ-the-king", christ_the_king y) ] + @ (match holy_name_sunday y with + | Some d -> [ ("ef-holy-name-sunday", d) ] + | None -> [ ("ef-holy-name", holy_name_fallback_date y) ]) (* Compile-time check that this module satisfies the kernel's rite contract. *) module _ : Colitur_kernel.Temporal.RITE = struct -- cgit v1.3 From 853351fad8aebc71c5341e6ff90371ca0caf8cf9 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 12:01:25 +0200 Subject: fix round 1: RG 110(c)'s ordering was inverted; M19 lacked an identity gate RG 110(c), word for word: "quoties vero oratio unius Apostoli addenda est ad modum commemorationis, huic orationi additur altera immediate, ante omnes alias commemorationes." "Huic orationi" (dative, "to this oration") refers to the trigger's own oration; "additur altera" means the companion is added to it, i.e. follows it. "Ante omnes alias commemorationes" governs the pair's position relative to other, unrelated commemorations, not the pair's own internal order. The previous build prepended the companion uniformly in both RG 110 shapes, citing this same clause for both -- correct for (a)/(b) (the day's own oration, not a list member, has nothing to order against), backwards for (c), where the trigger is itself a list member and the companion belongs after it. Fixed in rg110_additions: shape (a)/(b) still prepends; shape (c) now splices the companion in immediately after its own trigger. Re-pinned the unit test that had asserted the wrong order, and corrected both rows' comments to cite the clause that actually governs each shape. No layer in this codebase compares commemoration order: the lectio differential never compares commemorations, the oracle sorts both sides into a multiset, and the golden tests' own describe function sorts its commemorations field too. Recorded as a new, permanent blind spot in CLAUDE.md and the register, not merely fixed around -- the admit_cases unit table is the only place order is asserted at all. Re-measured the full blast radius against the same pre-change binary: same 14,627 differing days, same four shapes and counts, confirming the fix changed only the content of the 593 shape-(c) rows and nothing else. Mutation-tested: reverting the splice back to a uniform prepend reddens exactly the shape-(c) unit test; a new golden pin for a real shape-(c) day (2004-02-22) stays green under the same mutation, living proof that golden pins cannot see this axis, not merely a claim. Separately, M19's own predicate checked a date and a diff shape but not which candidate -- proved insufficient by fabricating a second, bogus companion on 30 June and confirming the whole suite stayed green. Fixed by requiring the admitted commemoration to be exactly commemoration-of-st-peter, mirroring C16's own identity guard. Added a golden pin for 30 June, the only assertion of that entry's identity outside the allow-list. Also: corrected a false claim in the task report that the electronic transcription carries no corroboration of the Holy Name fallback -- its own table of contents does, at a line not checked on the first pass. Added two further citations, both re-verified against the scan: RG 17's own closing paragraph, settling the Sunday shape's Sunday- exclusion at the rubric level rather than only in the propers; RG 124(c), which assigns red to 30 June by paragraph number rather than by the generic apostle/martyr default. Noted the fragility of deriving the Sunday shape's RG 109(a) privilege from a slug-substring match rather than from a citation. Cosmetic: a stray blank line and an indentation inconsistency. --- CLAUDE.md | 26 +++++++- data/ef/adjustments.sexp | 25 ++++---- lib/rites/rite_ef/precedence_ef.ml | 120 +++++++++++++++++++++++++------------ lib/rites/rite_ef/temporal_ef.ml | 37 +++++++++++- test/test_golden.ml | 52 ++++++++++++++++ test/test_oracle.ml | 28 ++++++++- test/test_precedence_ef.ml | 35 ++++++++--- 7 files changed, 262 insertions(+), 61 deletions(-) (limited to 'lib/rites/rite_ef/temporal_ef.ml') diff --git a/CLAUDE.md b/CLAUDE.md index f652a33..b809f00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,6 +113,16 @@ against missalemeum; layer 5 pins ~30 dates. the week column. It also **shares colitur's own lineage**: colitur's data was bootstrapped from lectio, so an error both inherited is invisible here. Proven: Holy Thursday was violet in both because both were wrong. +- **Nothing anywhere compares commemoration ORDER** (found `ef-holyname-rg110` + task, fix round 1, RG 110's own shape-(c) ordering bug): layer 3 doesn't compare + commemorations at all (above); layer 4's own `identity_diff` sorts both sides + into a multiset before comparing; layer 5's own `describe` (test_golden.ml) + sorts its `comms` field too, deliberately, so a golden pin's string comparison + is not accidentally order-sensitive where nothing textual requires it to be. + The ONLY place in the whole suite that asserts commemoration order is + `test_precedence_ef.ml`'s own `admit_cases` table (`Alcotest.(check (list + string))`, unsorted) — proven by mutation: reverting RG 110(c)'s own + trigger/companion order left every layer green except that one table. - **Layer 4's OBSERVED-identity gap is CLOSED** (2026-08-13, branch `ef-rg112-rg110`, register §6.2): it used to compare the observed day's rank and colour and stop there — never whether it is actually the RIGHT day. Holy @@ -424,8 +434,20 @@ nowhere in git (`docs/` is gitignored). commemorations); the oracle allow-list needed M12 removed, M15 widened by one date (a newly-exposed instance of its own pre-existing limit), and a new M19 for the 30-June gap. - -**Structural items, cheapest to fix before OF exists:** + **CORRECTED, fix round 1: RG 110(c)'s own ordering was inverted on all + 593 shape-(c) days.** *"Huic orationi additur altera"* (the companion is + added TO the trigger's own oration) means the trigger comes first, the + companion follows — the original build prepended the companion + uniformly in both shapes, right for (a)/(b) (nothing in the list to + order against) but backwards for (c). Fixed in `rg110_additions` + (splice the companion in immediately after its own trigger for shape + (c) only); the wrong-order unit test is re-pinned. New, permanent + blind spot found and recorded: **nothing in this suite compares + commemoration order except that one unit table** — see "know what each + layer cannot see" above. M19's own predicate was also strengthened to + check commemoration IDENTITY, not merely presence (the same C6/C14 + failure mode, proven by fabricating a second companion and watching the + whole suite stay green pre-fix). 5. **`Record` and `Liturgical_day` both claim to be "the single stable output schema".** `Record` cannot express what the engine now computes (no observed diff --git a/data/ef/adjustments.sexp b/data/ef/adjustments.sexp index 1451b40..740180c 100644 --- a/data/ef/adjustments.sexp +++ b/data/ef/adjustments.sexp @@ -149,16 +149,22 @@ ; year's `In Commemoratione Sancti Pauli Apostoli` row carries any ; commemoration -- missalemeum has the SAME gap this directive closes, ; verdict colitur in data/ef/expected-divergences-missalemeum.sexp's own -; M19). Colour Red: no primary-source pin for this specific field (a -; "Commemoratio" line's own colour is not independently stated by the -; calendarium the way a full feast's class-and-colour line is -- the same -; honestly-flagged inference shape this file's own `romanus`/`eusebius- +; M19). Colour Red: this ENTRY's own field has no independent primary-source +; pin (a bare "Commemoratio" line's own colour is not independently stated +; by the calendarium the way a full feast's class-and-colour line is -- the +; same honestly-flagged inference shape this file's own `romanus`/`eusebius- ; confessor` history already documents elsewhere in this project), chosen to -; match `paul`'s own entry (RG 117's ordinary apostle/martyr colour) and the -; host day's own Red -- inert either way today (Commemoration_only entries -; are never the OBSERVED celebration, so this colour is never printed by the -; current pipeline, the same note `Edit eusebius-confessor` above makes for -; its own colour field). +; match `paul`'s own entry and the HOST DAY's own Red -- and the host day's +; own Red IS now primary-sourced (fix round 1, coordinator finding F4), not +; merely RG 117's generic apostle/martyr default: RG 124(c), scan-verified +; word for word, `missale-romanum-1962.pdf`: "124. Item adhibetur color +; ruber in Officio et Missa de festis: ... c) Commemorationis S. Pauli +; Apostoli (30 iunii)" -- red is ALSO used in the Office and Mass of the +; feasts of ... (c) the Commemoration of St Paul the Apostle (30 June) -- +; naming this exact date by number. Inert either way today +; (Commemoration_only entries are never the OBSERVED celebration, so this +; entry's OWN colour is never printed by the current pipeline, the same +; note `Edit eusebius-confessor` above makes for its own colour field). ((id ef-adjustments) (directives ((Suppress vigil-of-christmas) @@ -172,4 +178,3 @@ ((en "St. Peter") (pl "\197\155w. Piotra, Aposto\197\130a"))) (rank Class3) (status Commemoration_only) (colour Red) (subject Saint) (citations ()) (layer ef-universal)))))))) - diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index c488596..cd0c53b 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -1125,37 +1125,73 @@ let rg110_find_companion comms companion_slug = (* RG 110's own uncapped addition, applied to [normal] -- the day's ALREADY- COMPUTED, ORDINARILY-CAPPED admission result (every branch of {!admit} below, unchanged otherwise). Two triggers, covering RG 110(a)/(b) and - RG 110(c) respectively, in this function's own comment above: + RG 110(c) respectively, in this function's own comment above -- and, + fix round 1, TWO DIFFERENT ORDERINGS, not one, because the two shapes' + own primary texts say different things about where the companion goes: + - [observed] itself is one of the three trigger slugs (shape (a)/(b): Peter's or Paul's own Office/Mass IS today's day) -- the companion is - looked up directly and appended, uncapped. + looked up directly and PREPENDED to [normal]. (a)/(b)'s own text, + "oratio alterius Apostoli additur ... orationi diei" (the OTHER + Apostle's oration is added ... TO THE DAY'S OWN oration), has nothing + in [normal] to order the companion AGAINST in the first place: the + day's own oration is [observed], not a member of this list, so + leading the list is simply where an item with nothing to be "before" + or "after" inside it goes -- not itself a citation for a POSITION + the primary text does not describe a list position for at all. - one of [normal]'s OWN members is a trigger slug (shape (c): Peter's or Paul's own office lost the day outright but is itself being admitted - as an ordinary/privileged commemoration of whatever else won) -- same - lookup, same append. - Both can never fire together on one call ({!Precedence.resolve} only - ever calls {!Precedence.rules.admit} once per date, and all three + as an ordinary/privileged commemoration of whatever else won) -- the + companion is looked up the same way, but SPLICED IN IMMEDIATELY AFTER + its own trigger, not prepended to the whole list. RG 110(c)'s own + text, word for word: "quoties vero oratio unius Apostoli addenda est + ad modum commemorationis, HUIC ORATIONI ADDITUR ALTERA immediate, ante + omnes alias commemorationes" -- "huic orationi" (dative, "to THIS + oration") refers back to "oratio unius Apostoli" (the trigger's OWN + oration, the one just named as "addenda... ad modum commemorationis") + -- so "additur altera" (the OTHER is added) means the companion is + added TO the trigger's own oration, i.e. FOLLOWS it, not precedes it; + the pair as a BLOCK then precedes "omnes ALIAS commemorationes" + ("alias" = OTHER, unrelated ones -- cannot include the pair's own + first member). CORRECTED, fix round 1: the original version of this + function prepended in BOTH shapes uniformly, citing (c)'s own "ante + omnes alias" for shape (a)/(b) too by analogy -- which is fine for + WHERE the pair sits relative to unrelated commemorations, but wrongly + also reordered the PAIR'S OWN internal order in shape (c), producing + `paul, chair-of-st-peter` where the primary text requires + `chair-of-st-peter, paul` (verified against BOTH photographic scans, + word for word, no divergence to adjudicate). Confirmed live: every one + of the 593 shape-(c) days measured in this task's own report emitted + the companion FIRST before this fix; RG 111's own admission branches + never build a second, unrelated commemoration alongside a shape-(c) + trigger in today's data (RG 111(b) admits exactly one candidate on a + II-class Sunday, the only day shape (c) ever occurs), so this + splice-after-trigger behaviour is Total the same way {!take}/ + {!drop_band} are, but has no OTHER live witness to also prove the + "ante omnes alias" half of (c) against. + + Both shapes can never fire together on one call ({!Precedence.resolve} + only ever calls {!Precedence.rules.admit} once per date, and all three trigger slugs are distinct FIXED calendar dates -- 25 January, 22 - February, 30 June -- so at most one of the two conditions is ever true - for a given [observed]/[normal] pair; [List.filter_map] over both - sources together is simply the shortest way to express "check every - place a trigger slug could legitimately appear", not evidence they are - expected to co-occur). - - PREPENDED, not appended, per RG 110(c)'s own explicit ordering directive - ("ante omnes alias commemorationes") -- the one clause in this rule that - actually specifies WHERE, restated here for shape (a)/(b) too rather than - drawing an unwarranted distinction between the two: no live data in this - codebase ever has a THIRD, unrelated commemoration alongside a RG 110 - pair to make the ordering choice visible in real output (the companion is - always the only OTHER item, or the only item, in [normal] already), so - this is closer to a defensive convention than a tested outcome -- flagged - honestly rather than claimed as independently verified. + February, 30 June -- so at most one of [observed]/[normal]'s own members + is ever a trigger for a given call). A companion already present in [normal] on its own merits (structurally unreachable today -- {!unclassified}'s own comment above -- but not provably so for every future rite/data shape) is not added a second - time: [List.exists] guards each append. *) + time: [List.exists] guards both branches. + + NEW BLIND SPOT, fix round 1: no validation layer in this codebase + compares commemoration ORDER at all -- the lectio differential does not + compare commemorations (its own "limit 1"); the oracle's own + [identity_diff] sorts both sides into a multiset before comparing; + {!Colitur_kernel.Record}/`describe` (test_golden.ml) both sort their own + comms field too, for the same reason presence/identity checks should not + be order-sensitive by accident. This function's own [admit_cases] unit + test (test_precedence_ef.ml) is therefore the ONLY place in this + codebase's test suite where commemoration order is asserted at all -- + flagged here, and in CLAUDE.md's own "know what each layer cannot see" + section, as a genuine, permanent limit, not merely this fix's own gap. *) let rg110_additions (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) ~(observed : Vocab_ef.rank Precedence.candidate) (normal : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) : @@ -1163,20 +1199,28 @@ let rg110_additions (comms : (Vocab_ef.rank Precedence.candidate * Precedence.pr let already_has slug = List.exists (fun (c, _) -> Slug.to_string c.Precedence.cel.Celebration.slug = slug) normal in - let trigger_slugs = - Slug.to_string observed.Precedence.cel.Celebration.slug - :: List.map (fun (c, _) -> Slug.to_string c.Precedence.cel.Celebration.slug) normal - in - let companions = - List.filter_map - (fun trigger_slug -> - match rg110_companion_slug trigger_slug with - | None -> None - | Some companion_slug -> - if already_has companion_slug then None else rg110_find_companion comms companion_slug) - trigger_slugs - in - companions @ normal + match rg110_companion_slug (Slug.to_string observed.Precedence.cel.Celebration.slug) with + | Some companion_slug when not (already_has companion_slug) -> ( + (* Shape (a)/(b): prepend -- see this function's own comment above. *) + match rg110_find_companion comms companion_slug with + | Some companion -> companion :: normal + | None -> normal) + | _ -> + (* Shape (c): splice each trigger's companion in immediately after it + -- {!List.concat_map} rather than a fold, so a trigger with no + companion found (defensive; {!rg110_find_companion} returning + [None]) simply passes through unchanged, and a member that is not + a trigger at all ([rg110_companion_slug] returns [None]) is never + touched. *) + List.concat_map + (fun ((c, _) as pair) -> + match rg110_companion_slug (Slug.to_string c.Precedence.cel.Celebration.slug) with + | Some companion_slug when not (already_has companion_slug) -> ( + match rg110_find_companion comms companion_slug with + | Some companion -> [ pair; companion ] + | None -> [ pair ]) + | _ -> [ pair ]) + normal let admit ~(observed : Vocab_ef.rank Precedence.candidate) ~(temporal : Vocab_ef.rank Precedence.candidate) @@ -1227,7 +1271,7 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) due; at most one privileged one does, the highest-precedence one (RG 113: {!band}'s own table order) if several are. *) (match List.filter is_privileged sorted with [] -> [] | best :: _ -> [ drop_band best ]) - | Class2, true -> + | Class2, true -> (* RG 111(b), primary text, RE-VERIFIED word for word against the scan (final fix wave; this sentence is the sole textual basis for the shipped rank-floor fix below, and the register's own §4 "RG 111" @@ -1268,7 +1312,7 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) match List.filter (fun (c, _, _) -> c.Precedence.cel.Celebration.rank = Class2) sorted with | [] -> [] | best :: _ -> [ drop_band best ])) - | Class2, false -> + | Class2, false -> (* RG 111: "other II class: one" -- no privilege-override clause here, unlike the Sunday case immediately above, so the day's one slot goes to whichever candidate outranks the rest by RG 113's own diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml index 721790d..bbd88ec 100644 --- a/lib/rites/rite_ef/temporal_ef.ml +++ b/lib/rites/rite_ef/temporal_ef.ml @@ -598,7 +598,42 @@ let temporal d = check this way, against the SAME independently-computed date [holy_name_sunday] returns rather than a bare month/ day range, is what test_temporal_ef.ml's own anchor test - below cross-checks against. *) + below cross-checks against. + + ADDED, fix round 1 (coordinator finding F4): the Sunday + shape's "takes the Sunday's own place, no commemoration of + it" treatment is settled at RG LEVEL, not only in Holy + Name's own Mass propers (this file's own [holy_name_sunday] + citation already has that quote) -- RG 17's own CLOSING + paragraph, immediately after its own lettered list + (a)-(e), scan-verified: "Haec festa locum tenent dominicae + occurrentis cum omnibus iuribus et privilegiis; de + dominica, proinde, nulla fit commemoratio" -- "these + feasts" (plural, covering the WHOLE list (a)-(e), Holy Name + included) "hold the place of the occurring Sunday with all + rights and privileges; of the Sunday, therefore, no + commemoration is made". A second, independent primary + source for the same conclusion the propers-level quote + already gives, not a new claim. + + FRAGILE DERIVATION, noted per fix round 1 (coordinator + finding F5): the mechanism that actually grants a LOSING + Holy-Name-Sunday its RG 109(a) privilege when outranked + (Precedence_ef.privilege_of's own (a) branch, {!Precedence + _ef.is_sunday_slug}) reads it off the "-sunday" SUBSTRING + in the slug ("ef-holy-name-sunday") -- a naming convention, + not a citation of RG 17's own text quoted immediately + above, which is the actual warrant ("these feasts hold the + place of the occurring Sunday WITH ALL ITS RIGHTS AND + PRIVILEGES" -- RG 109(a)'s own "of a Sunday" privilege is + one of those rights). Right answer, fragile path: a future + rename of this slug family would silently drop the + privilege with no compiler or test failure pointing here. + No live witness exists to test it either way (no + fixed-date candidate ever outranks Holy Name in today's + data -- test_precedence_ef.ml's own synthetic + [test_class1_feast_inside_holy_name_window_end_to_end] + proves the SYNTHETIC case only). *) let subject, names = if same d (holy_family_sunday y) then ( Colitur_kernel.Subject.Lord, diff --git a/test/test_golden.ml b/test/test_golden.ml index 80e2c9d..0f90ee7 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -691,6 +691,51 @@ let test_holy_name_fallback_2029 () = "2029-01-02 tuesday season=christmastide week=- slug=ef-holy-name rank=class-2 colour=white \ subject=lord comms=[] in=- out=[]" +(* RG 110 (docs/research/rules-register.md §4, Caput XIV), fix round 1 + (coordinator findings F1/F2): two golden pins neither existed before this + round -- F2 named 30 June's own total absence of golden coverage + directly ("no golden pin on any 30 June date"), and F1 named the + shape-(c) collision as a real day this suite otherwise never exercises + end-to-end against real data (both new [admit_cases] rows are synthetic + candidates, not `Calendar.day` over the real committed layer). + + *** BOTH PINS ARE BLIND TO COMMEMORATION ORDER -- STATED HONESTLY, NOT + CLAIMED AS PROOF OF F1's OWN FIX *** + + [describe]'s own `comms` field (this file's header, above) sorts the + commemoration list alphabetically before rendering + (`List.sort compare`) -- deliberately, so a golden pin's own STRING + comparison is not accidentally order-sensitive where nothing textual + requires it to be. That means this file, like the differential and the + oracle's own [identity_diff] (test_oracle.ml), CANNOT see whether + [rg110_additions] emits `chair-of-st-peter, paul` or `paul, + chair-of-st-peter` -- both sort to the same string. The ONLY place in + this codebase's test suite that asserts commemoration ORDER at all is + `test_precedence_ef.ml`'s own [admit_cases] table (`Alcotest.(check + (list string))`, not a sorted string) -- see precedence_ef.ml's own + [rg110_additions] comment for the citation this uses to fix the order, + and CLAUDE.md's "know what each layer cannot see" section for this as a + permanent, structural limit, not a gap this task's own pins happened not + to close. These two pins prove PRESENCE and IDENTITY of both + commemorations on a real, `Calendar.day`-resolved date -- genuine + ground no other pin in this file currently covers -- not order. *) +let test_rg110_june_30_2026 () = + check ~msg:"2026-06-30: RG 110's 30-June direction -- In Commemoratione Sancti Pauli Apostoli observed, \ + +commemoration-of-st-peter (data/ef/adjustments.sexp's own `Add` directive) the sole \ + commemoration -- missalemeum shows neither (M19, verdict colitur)" + 2026 6 30 + "2026-06-30 tuesday season=time-after-pentecost week=5 slug=in-commemoratione-sancti-pauli-apostoli \ + rank=class-3 colour=red subject=saint comms=[commemoration-of-st-peter:ordinary] in=- out=[]" + +let test_rg110_shape_c_2004 () = + check ~msg:"2004-02-22: RG 110 shape (c) -- Chair of St Peter itself loses the day to an ordinary \ + (Septuagesima, not I-class) II-class Sunday and is admitted only as a commemoration under \ + RG 111(b)'s own rank floor; Paul rides along too, uncapped (`date -d 2004-02-22 +%A` = \ + Sunday, independently confirmed)" + 2004 2 22 + "2004-02-22 sunday season=septuagesima week=3 slug=ef-septuagesima-sunday-3 rank=class-2 colour=violet \ + subject=temporal comms=[chair-of-st-peter:ordinary,paul:ordinary] in=- out=[]" + (* 2025-02-02, the other real instance of the SAME pattern -- not a contrasting second direction, per this task's fix round 1 (CRITICAL finding, item 1, reverted; see register §6.0 for the full account). @@ -856,6 +901,13 @@ let suite = "Holy Name of Jesus, fallback shape (RG17(a) \"secus die 2 ianuarii\", 2029-01-02 -- no Sunday in \ the window)" `Quick test_holy_name_fallback_2029; + Alcotest.test_case + "RG110: 30 June, In Commemoratione S. Pauli Apostoli + commemoration-of-st-peter (2026-06-30)" `Quick + test_rg110_june_30_2026; + Alcotest.test_case + "RG110 shape (c): Chair of St Peter admitted only as a commemoration of an ordinary Sunday, Paul \ + rides along (2004-02-22)" + `Quick test_rg110_shape_c_2004; Alcotest.test_case "ef-rebootstrap: RG91 entry24 (III-class feast) outranks entry28 (class-4 feria) -- the six dates \ deferred since 2038's own Easter-extreme pin (2008-04-02/04/05, 2038-03-06/08/09)" diff --git a/test/test_oracle.ml b/test/test_oracle.ml index 370eec0..58ceb26 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -796,9 +796,32 @@ let m16_dates = [ "2026-03-27" ] citation-count coincidence: RG 110's own text is unconditional ("in Officio et Missa S. Petri semper fit commemoratio S. Pauli, ET VICISSIM"), and the calendarium's own June table states the SAME - pattern already built for the other two pairs, word for word. *) + pattern already built for the other two pairs, word for word. + + IDENTITY-GATED, fix round 1 (coordinator finding F2): the predicate + below now also requires [c.c_commemorations] to be EXACTLY the single + entry [commemoration-of-st-peter] -- not merely that some [Comm_presence] + diff exists on these two dates. Proved necessary, not decorative: the + reviewer added a SECOND, fabricated `Add` directive on 30 June (a + `bogus-fabricated-companion` slug) to a scratch copy of + data/ef/adjustments.sexp and found the whole suite stayed green with the + pre-fix, presence-only predicate -- the exact C6/C14 failure mode (a + Layer-C-style predicate that pins a date and a diff SHAPE but not WHICH + candidate). This matters more here than for any other entry in this + file: `commemoration-of-st-peter` is corroborated by NEITHER oracle (it + rests on a scan reading alone, data/ef/adjustments.sexp's own citation), + so this allow-list predicate is the only place in the whole suite that + could assert what colitur actually emits here -- and, pre-fix, asserted + nothing about it. Mirrors C16's own identity guard + (`String.equal c.slug "ef-holy-name"`, test_differential.ml) at the + analogous decision point in this file. *) let m19_dates = [ "2026-06-30"; "2027-06-30" ] +let m19_commemoration_matches (c : colitur_row) = + match c.c_commemorations with + | [ (slug, _, _, _) ] -> String.equal slug "commemoration-of-st-peter" + | _ -> false + let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = if diffs = [] then None (* M1's own subset widened (this task): colitur's observed day on both @@ -850,7 +873,8 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = && subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ] then Some "M16" else if diffs = [ Observed_identity_unresolved ] then Some "M18" - else if List.mem c.c_date m19_dates && diffs = [ Comm_presence ] then Some "M19" + else if List.mem c.c_date m19_dates && diffs = [ Comm_presence ] && m19_commemoration_matches c then + Some "M19" else None (* ---------------------------------------------------------------------- *) diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index af7f65e..639f64c 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -1146,8 +1146,14 @@ let admit_cases = precedence_ef.ml's own measured defect (3,533 of 8,417 domain-wide years, this task's own report): Paul loses the slot outright and is never admitted at all. With RG 110, Paul is added ADDITIONALLY, - PREPENDED ahead of the feria per RG 110(c)'s own "ante omnes alias - commemorationes" -- both admitted, not one displacing the other. *) + PREPENDED ahead of the feria -- RG 110(a)/(b)'s own text ("oratio + alterius Apostoli additur ... orationi diei", the companion's oration + added to the DAY's own oration) governs here, not (c)'s "ante omnes + alias commemorationes" (CORRECTED, fix round 1, coordinator finding + F1: this comment previously cited (c) for both shapes uniformly -- + see [rg110_additions]'s own comment in precedence_ef.ml for why the + two shapes need, and get, DIFFERENT orderings, not the same one for + different reasons) -- both admitted, not one displacing the other. *) ( "RG110 (a)/(b): chair-of-st-peter observed, Paul rides alongside a \ competing privileged Lent feria, uncapped and prepended", chair_of_st_peter_synth, chair_of_st_peter_synth, @@ -1157,16 +1163,29 @@ let admit_cases = (observed is some unrelated II-class Sunday) but is admitted, on its OWN Class2 dignity, as RG 111(b)'s single ordinary commemoration -- exactly the domain-wide 593-year pattern this task's own report - measures. RG 110(c)'s own text ("quoties vero oratio unius Apostoli - addenda est ad modum commemorationis...") governs precisely this: the + measures. RG 110(c)'s own text, word for word ("quoties vero oratio + unius Apostoli addenda est ad modum commemorationis, HUIC ORATIONI + ADDITUR ALTERA immediate, ante omnes alias commemorationes"): the companion (Paul, Class3, no standing of its OWN for this Sunday's - rank-floored slot) rides along anyway, uncapped. *) + rank-floored slot) rides along anyway, uncapped -- ADDED TO THE + TRIGGER'S own oration ("huic orationi additur altera"), i.e. + FOLLOWING it, not preceding it. CORRECTED, fix round 1 (coordinator + finding F1): this row originally asserted `["paul"; + "chair-of-st-peter"]` -- the companion BEFORE the trigger -- citing + the SAME "ante omnes alias commemorationes" clause that actually + governs the PAIR's position relative to OTHER, unrelated + commemorations, not the pair's own internal order. "Huic orationi" + (dative, "to THIS oration") refers back to "oratio unius Apostoli", + the trigger's own oration -- so the companion is added TO it, + following, not leading. Reproduced by mutation: reverting + [rg110_additions]'s shape-(c) branch back to a uniform prepend + reddens exactly this row's own expected order, nothing else. *) ( "RG110 (c): chair-of-st-peter admitted merely AS a commemoration \ - (loses the day to an ordinary II-class Sunday), Paul still rides \ - along", + (loses the day to an ordinary II-class Sunday), Paul rides along \ + AFTER it (RG110(c): \"huic orationi additur altera\")", cand ~rank:V.Class2 "ef-some-other-sunday", cand ~rank:V.Class2 "ef-some-other-sunday", [ po chair_of_st_peter_synth P.Ordinary; (paul_companion_synth, P.Ordinary, 999) ], - [ "paul"; "chair-of-st-peter" ] ); + [ "chair-of-st-peter"; "paul" ] ); (* Defensive: a trigger slug is [observed], but its companion is simply not present among [comms] at all (a data gap, or -- as this exact shape was for 30 June before this task's own [Add -- cgit v1.3