From bd7a41b5b83a5fc18e6169327c9176eee2d54a2b Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 08:07:17 +0200 Subject: test(oracle): compare the observed day's identity, not only rank/colour Layer 4 (test_oracle.ml, vs missalemeum) compared the observed celebration's rank and colour and stopped there -- it never checked whether the observed day is actually the RIGHT one. Holy Family (11 January 2026: oracle title "The Holy Family: Jesus, Mary & Joseph") is rank 2/white on both sides purely by coincidence -- an ordinary, unnamed Sunday and Holy Family share both -- so this layer has been silently blind to the missing feast since it was built. Adds Observed_identity_mismatch/Observed_identity_unresolved, following the shape and honesty of yesterday's commemoration-identity comparison (Comm_identity_mismatch/Comm_identity_unresolved): colitur identifies its own observed day by slug, missalemeum by an English title, and the bridge is Celebration.t.names, read directly off LD.observed. That name is only ever set for a SANCTORAL-origin celebration -- a TEMPORAL-origin one (an ordinary Sunday, a feria, a movable named feast) carries none, so most of the 730 days in the fixture window land in Observed_identity_unresolved, not a clean match. That is counted and reported, never silently skipped -- the same discipline this file already applies to an unresolvable commemoration. WIP, expected red: 399 days now show observed-identity-unresolved with nothing in the allow-list naming it, and five of the six existing M-series entries whose own diff shape is checked by exact list equality (M1, M2, M3, M13, M16) lose their match now that this axis widens their diff set. Landed on its own, ahead of any production fix, so it is the regression net the following commits are checked against rather than a change bundled in alongside the fix they enable. The next commits widen those five entries' own diff shapes and add the new bucket's own cited allow-list entry, then build Holy Family itself. --- test/test_oracle.ml | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/test/test_oracle.ml b/test/test_oracle.ml index a86fd76..ae5c396 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -3,6 +3,20 @@ layer 4 of the design spec's five (colitur CLAUDE.md "Validation" section; layer 3, the lectio differential, is test_differential.ml, already green). + *** WIP, 2026-08-13, branch ef-rg112-rg110: this file is EXPECTED RED + right now. [Observed_identity_mismatch]/[Observed_identity_unresolved] + (below) are new -- this layer used to compare the observed day's rank and + colour and stop there, never whether it is actually the RIGHT day (Holy + Family, 11 January 2026, is rank 2/white on both sides purely by + coincidence -- an ordinary unnamed Sunday and Holy Family share both). + Landing the strengthened comparator FIRST, red, before touching any + production code, is the point: it is the regression net the rest of this + task's fix commits are checked against, not a change bundled in alongside + the fix. The very next commits make it green again (data/ef/expected- + divergences-missalemeum.sexp's own M-series entries below, and this + file's own [layer_m_reason] widening) -- see that file and + docs/research/rules-register.md for the running account. + *** WHY THIS LAYER EXISTS, AND WHAT IT CAN DO THAT THE LECTIO DIFFERENTIAL CANNOT (test_differential.ml's own header comment, limit 1) *** @@ -280,6 +294,15 @@ type colitur_row = { c_date : string; c_rank : int; c_colour : char; + c_observed_slug : string; + c_observed_name : string option; + (** colitur's own resolved English name for the OBSERVED celebration -- + [None] for a TEMPORAL-origin day (an ordinary Sunday, a feria, a + movable named feast: {!Rite_ef.Temporal_ef}'s own [build] never + sets [names], the SAME gap this file's header already documents + for commemorations, see [Comm_identity_unresolved]), the same + [Names.find ... en] read [c_commemorations] below already uses, + applied to [LD.observed] instead of a commemoration candidate. *) c_commemorations : (string * int * Cel.status * string option) list; (** slug, rank, status, English name *) } @@ -321,6 +344,8 @@ let colitur_rows_2026_2027 () = { c_date = Date.to_iso8601 day.LD.date; c_rank = rank_to_int cel.Cel.rank; c_colour = colour_to_char cel.Cel.colour; + c_observed_slug = Slug.to_string cel.Cel.slug; + c_observed_name = Names.find cel.Cel.names en; c_commemorations = commemorations } :: !rows @@ -335,7 +360,15 @@ let colitur_rows_2026_2027 () = (* header for the mapping and its exact limits. *) (* ---------------------------------------------------------------------- *) -type field = Rank | Colour_f | Comm_presence | Comm_count | Comm_identity_mismatch | Comm_identity_unresolved +type field = + | Rank + | Colour_f + | Comm_presence + | Comm_count + | Comm_identity_mismatch + | Comm_identity_unresolved + | Observed_identity_mismatch + | Observed_identity_unresolved let field_name = function | Rank -> "rank" @@ -344,6 +377,40 @@ let field_name = function | Comm_count -> "commemoration-count" | Comm_identity_mismatch -> "commemoration-identity-mismatch" | Comm_identity_unresolved -> "commemoration-identity-unresolved" + | Observed_identity_mismatch -> "observed-identity-mismatch" + | Observed_identity_unresolved -> "observed-identity-unresolved" + +(* CORRECTING the gap this file's own header names (added under this task, + branch ef-rg112-rg110): the axes above all compare the day's OBSERVED + celebration's rank and colour ([Rank]/[Colour_f]) and the admitted + COMMEMORATIONS' identity ([Comm_identity_*]) -- nothing until now compared + the OBSERVED celebration's own identity, WHICH day is actually being kept, + only what class/colour it happens to carry. Holy Family (11 January 2026: + oracle title "The Holy Family: Jesus, Mary & Joseph", colitur's own + observed slug the plain "ef-time-after-epiphany-sunday-1", BOTH rank 2 and + colour white on both sides) is exactly the shape this axis exists to + catch -- rank and colour already agreed by coincidence (an ordinary, + unnamed II-class Sunday and Holy Family share both), so nothing above + ever saw a difference. Same mapping and the same honesty about its limit + as [identity_diff] just above: colitur identifies a day by SLUG, + missalemeum by an English TITLE string, and the bridge is + {!Celebration.t}.names, read directly off [LD.observed] by + [colitur_rows_2026_2027] into [c_observed_name] -- resolvable for a + SANCTORAL-origin observed day (a saint's feast winning the day outright), + NEVER for a TEMPORAL-origin one (an ordinary Sunday, a feria, a movable + named feast -- {!Rite_ef.Temporal_ef}'s own [build] never sets [names]). + [Observed_identity_unresolved] is that TEMPORAL-origin case: counted and + reported, never silently skipped, the same discipline + [Comm_identity_unresolved] already established and the brief demands + again here ("a day whose observed identity cannot be resolved must be a + counted, allow-listed outcome, never a silent skip"). Unlike commemoration + identity, this axis needs no presence/count gate first -- there is always + exactly one observed day on each side, no list-length ambiguity to + resolve before a title comparison is even meaningful. *) +let observed_identity_diff (c : colitur_row) (o : oracle_row) = + match c.c_observed_name with + | None -> Some Observed_identity_unresolved + | Some name -> if String.equal name o.o_title then None else Some Observed_identity_mismatch (* Identity (Task B): only even attempted once presence AND count already agree (both non-empty, same length) -- see this file's own header, @@ -378,7 +445,8 @@ let diff_fields (c : colitur_row) (o : oracle_row) = (if c_has && o_has && List.length c.c_commemorations <> List.length o.o_commemorations then Some Comm_count else None); - identity_diff c o + identity_diff c o; + observed_identity_diff c o ] (* ---------------------------------------------------------------------- *) -- cgit v1.3 From bbc8ce26916ca92b3c2286c57342a6565323b66f Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 08:11:26 +0200 Subject: temporal(ef): the Holy Family, first Sunday after Epiphany (RG 17(b)) RG 17(b), verified against both photographic scans (missale-romanum- 1962.pdf and "Missale Romanum 1962_text.pdf" -- the electronic transcription carries the same paragraph, so this one is not itself a transcription-vs-scan divergence): the Holy Family of Jesus, Mary and Joseph is celebrated on the first Sunday after Epiphany, II class, EVERY year, taking the Sunday's own place "cum omnibus iuribus et privilegiis" -- the same formula RG 16(a) already uses. The slug, rank and colour temporal_ef.ml already computed for that Sunday (ef-time-after-epiphany-sunday-1, Class2, white) turn out to be exactly right by coincidence -- an ordinary, unnamed Sunday and Holy Family share both. The only field that was silently wrong is subject: always Temporal before this, when RG 91 entry 14 ("Festa Domini II classis, primum mobilia, deinde fixa") calls Holy Family a movable Feast of the Lord. temporal's build now takes an optional ~subject, applied only on Holy Family's own date -- every other Sunday, including the still-unbuilt Holy Name of Jesus (RG 17(a)), is unaffected. RG 17(b)'s window (7-13 January) can never be empty of a Sunday -- sunday_on_or_before(6 Jan) + 7 always lands in [7, 13] regardless of 6 January's weekday -- unlike RG 17(a)'s Holy Name (2-5 January, CAN be empty), whose calendarium entry carries an explicit "vel ea deficiente, die 2 ianuarii" fallback right next to Holy Family's own, fallback-free one. No fallback needed or built. Added to anchors (holy_family_sunday is independently computed, not routed through named -- see temporal_ef.ml's own comment on why) and to its own dedicated anchor/erosion test in test_temporal_ef.ml, kept separate from the generic named-days coverage test since this genuinely isn't one of named's own outputs. Does not yet touch precedence: band still has no notion of a movable Class2 Lord feast (entry 14 gates on sanctoral origin only), so on the seven years 13 January is itself Holy Family's date (2008, 2013, 2019, 2030, 2036, 2041, 2047), the fixed Commemoration of the Baptism of the Lord still wins the day exactly as before this commit -- the next commit fixes the ordering RG 91 entry 14's own text states ("primum mobilia, deinde fixa"). --- lib/rites/rite_ef/temporal_ef.ml | 89 +++++++++++++++++++++++++---- lib/rites/rite_ef/temporal_ef.mli | 7 +++ test/test_temporal_ef.ml | 116 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 11 deletions(-) (limited to 'test') diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml index 7782876..0647aac 100644 --- a/lib/rites/rite_ef/temporal_ef.ml +++ b/lib/rites/rite_ef/temporal_ef.ml @@ -64,6 +64,46 @@ let season d = citation gap. *) let christ_the_king y = sunday_on_or_before (mk y 10 31) +(* RG 17(b) (Caput III, "De Dominicis"), primary text, verified against BOTH + photographic scans (missale-romanum-1962.pdf and "Missale Romanum + 1962_text.pdf" -- the electronic transcription, 1962-06-23,_SS_Ioannes + _XXIII,_Missale_Romanum,_LT.pdf, carries the SAME text here, so this one + paragraph is not itself a transcription-vs-scan divergence): "17. + Dominica excludit, per se, assignationem perpetuam festorum. Excipiuntur: + a) festum Ssmi Nominis Iesu, celebrandum dominica quae occurrit a die 2 + ad 5 ianuarii (secus die 2 ianuarii); b) festum S. Familiae Iesu, Mariae, + Ioseph, celebrandum dominica prima post Epiphaniam; ... Haec festa locum + tenent dominicae occurrentis cum omnibus iuribus et privilegiis: de + dominica, proinde, nulla fit commemoratio" -- a Sunday of itself excludes + the perpetual assignment of feasts to it; EXCEPTED: (a) the Most Holy + Name of Jesus, celebrated on the Sunday falling 2-5 January (otherwise 2 + January); (b) the HOLY FAMILY of Jesus, Mary and Joseph, celebrated on + the FIRST SUNDAY AFTER EPIPHANY; ... these feasts hold the place of the + occurring Sunday with ALL its rights and privileges: of the Sunday, + therefore, NO commemoration is made -- the identical "cum omnibus + iuribus et privilegiis: nulla fit commemoratio" formula RG 16(a) already + uses for a FIXED-date Lord feast impeding a Sunday (register §6.0), + stated here for the Sunday-ASSIGNED feasts RG 17 itself lists by letter. + + UNLIKE (a)'s own narrow 2-5 January window (which CAN be empty of a + Sunday -- the calendarium's own January table carries an explicit "vel ea + deficiente, die 2 ianuarii" fallback for it, scan-verified), (b)'s 7-13 + January window can NEVER be empty: [sunday_on_or_before (mk y 1 6)] is, + by construction, at most 6 days before 6 January, so adding 7 always + lands in [7, 13] regardless of which weekday 6 January falls on (every + one of the 7 possible cases is exercised by test_temporal_ef.ml's own + [test_holy_family]). The calendarium's own text for (b), both in RG 17 + itself and in its January table entry ("Dominica I post Epiphaniam: + S. Familiae, Iesu, Mariae, Ioseph, II classis"), carries no fallback + clause of any kind -- consistent with there being no gap for one to + cover. + + Formula IDENTICAL to [week_origin]'s own [Time_after_epiphany] case below + (the first Sunday after Epiphany) -- not re-derived a second time, just + named here for its own citation and so [temporal] can test a specific + 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 + 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), @@ -404,15 +444,15 @@ let temporal d = let easter = Computus.gregorian_easter y in let s = season d in let weekday = Date.weekday d in - let build ~season ~slug ~colour ~rank ~week = + let build ?(subject = Colitur_kernel.Subject.Temporal) ~season ~slug ~colour ~rank ~week () = let office = - Colitur_kernel.Celebration.make ~slug:(Slug.of_string_exn slug) ~rank ~colour - ~subject:Colitur_kernel.Subject.Temporal ~layer:"temporal" () + Colitur_kernel.Celebration.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject + ~layer:"temporal" () in { Colitur_kernel.Temporal.season; week; weekday; office } in match named d with - | Some (season, slug, colour, rank) -> build ~season ~slug ~colour ~rank ~week:(week d) + | Some (season, slug, colour, rank) -> build ~season ~slug ~colour ~rank ~week:(week d) () | None -> ( (* Rogations (the Minor Litanies only -- RG 87, Monday and Tuesday before Ascension). The Major Litanies (25 April, RG 80) are a fixed @@ -441,10 +481,10 @@ let temporal d = if rogation = 36 || rogation = 37 then build ~season:s ~slug:(if rogation = 36 then "ef-rogation-monday" else "ef-rogation-tuesday") - ~colour:Colour.Violet ~rank:(ferial_rank d s) ~week:(week d) + ~colour:Colour.Violet ~rank:(ferial_rank d s) ~week:(week d) () else match ember d with - | Some (slug, rank, colour) -> build ~season:s ~slug ~colour ~rank ~week:(week d) + | Some (slug, rank, colour) -> build ~season:s ~slug ~colour ~rank ~week:(week d) () | None -> ( match sunday_slug d with | Some slug -> @@ -456,11 +496,29 @@ let temporal d = reaches this fallback -- leaving II class here except the remaining Advent and Lent Sundays. *) let rank = match s with Advent | Lent -> Class1 | _ -> Class2 in - build ~season:s ~slug ~colour ~rank ~week:(week d) + (* RG 17(b) (this file's own [holy_family_sunday], full + citation there): the ONE Sunday a year this branch must + NOT leave [subject = Temporal] (this function's own + default) -- the Holy Family, whose slug/rank/colour are + otherwise EXACTLY what this branch already computes for + the 7-13 January Sunday (an accident this task's own + oracle-strengthening work exposed: rank/colour alone + could never distinguish "Holy Family" from "an ordinary, + unnamed Sunday", since they happen to coincide). Every + OTHER Sunday this branch ever builds -- including the + narrower Holy Name of Jesus window, RG 17(a), still + unbuilt as its own named day, register §6 -- keeps + [Temporal], unaffected. *) + let subject = + if same d (holy_family_sunday y) then Colitur_kernel.Subject.Lord + else Colitur_kernel.Subject.Temporal + in + build ~subject ~season:s ~slug ~colour ~rank ~week:(week d) () | None -> ( match christmastide_feria_slug d with | Some slug -> - build ~season:s ~slug ~colour:(season_colour s) ~rank:(ferial_rank d s) ~week:(week d) + build ~season:s ~slug ~colour:(season_colour s) ~rank:(ferial_rank d s) + ~week:(week d) () | None -> (* The days between Ash Wednesday and Lent I have proper Masses and belong to no numbered week. *) @@ -468,7 +526,7 @@ let temporal d = if after_ashes >= -45 && after_ashes <= -43 then build ~season:s ~slug:(Printf.sprintf "ef-lent-after-ashes-%s" (weekday_word d)) - ~colour:Colour.Violet ~rank:Class3 ~week:None + ~colour:Colour.Violet ~rank:Class3 ~week:None () else let colour = (* The Pentecost octave weekdays are red, not Paschaltide's white. *) @@ -508,14 +566,22 @@ let temporal d = Printf.sprintf "ef-%s-%d-%s" (season_slug_word s) (Option.value week_n ~default:0) (weekday_word d) in - build ~season:s ~slug ~colour ~rank:(ferial_rank d s) ~week:week_n))) + build ~season:s ~slug ~colour ~rank:(ferial_rank d s) ~week:week_n ()))) (* Independent restatement of [named]'s fixed and Easter-relative dates, paired with the slug each should carry, for civil year [y]. Deliberately NOT derived from [named] itself -- consumed by [Validate]'s anchor- agreement check (design spec §5.7), which exists precisely to catch an accidental single-site drift (e.g. Ascension's [off 39] silently becoming - [off 40]) that both sides moving together would hide. *) + [off 40]) that both sides moving together would hide. + + [holy_family_sunday] is NOT one of [named]'s own outputs (its own + citation above explains why -- [temporal] applies it as a targeted + [subject] override inside [sunday_slug]'s branch, not through [named]'s + 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. *) let anchors y = let easter = Computus.gregorian_easter y in let off n = Date.add_days easter n in @@ -526,6 +592,7 @@ let anchors y = ("ef-nativity-octave-day-7", mk y 12 31); ("ef-circumcision", mk y 1 1); ("ef-epiphany", mk y 1 6); + ("ef-time-after-epiphany-sunday-1", holy_family_sunday y); ("ef-ash-wednesday", off (-46)); ("ef-passion-sunday", off (-14)); ("ef-palm-sunday", off (-7)); diff --git a/lib/rites/rite_ef/temporal_ef.mli b/lib/rites/rite_ef/temporal_ef.mli index a07ffa2..68fe64e 100644 --- a/lib/rites/rite_ef/temporal_ef.mli +++ b/lib/rites/rite_ef/temporal_ef.mli @@ -14,6 +14,13 @@ val season : Date.t -> Vocab_ef.season (** Last Sunday of October, per the 1960 calendar. *) val christ_the_king : int -> Date.t +(** RG 17(b): the first Sunday after Epiphany, in civil year [y] -- the + Feast of the Holy Family's own date. Always falls 7-13 January + inclusive; never empty of a Sunday (unlike RG 17(a)'s Holy Name, whose + narrower 2-5 January window can be, and carries an explicit calendarium + fallback for it), so this needs none. *) +val holy_family_sunday : int -> Date.t + (** The named temporal days: I-class feasts of the Lord, vigils, and days within the Octave of the Nativity. Returns (season, slug, colour, rank). Carries no week of its own -- {!temporal} computes it uniformly via {!week} for diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml index 751abd2..7837fb5 100644 --- a/test/test_temporal_ef.ml +++ b/test/test_temporal_ef.ml @@ -167,13 +167,95 @@ let test_resumed_sundays () = module Cel = Colitur_kernel.Celebration module Sl = Colitur_kernel.Slug module Colr = Colitur_kernel.Colour +module Sub = Colitur_kernel.Subject let office dt = (T.temporal dt).Colitur_kernel.Temporal.office let slug_of dt = Sl.to_string (office dt).Cel.slug let rank_of dt = V.rank_to_string (office dt).Cel.rank let colour_of dt = Colr.to_string (office dt).Cel.colour +let subject_of dt = (office dt).Cel.subject let temporal_week dt = (T.temporal dt).Colitur_kernel.Temporal.week +(* RG 17(b) (Caput III, "De Dominicis"), primary text (scan-verified, + docs/research/rules-register.md §4): "festum S. Familiae Iesu, Mariae, + Ioseph, celebrandum dominica prima post Epiphaniam" -- the Holy Family is + celebrated on the first Sunday after Epiphany, EVERY year (unlike RG + 17(a)'s Holy Name, whose narrower 2-5 January window can be empty and + carries an explicit "vel ea deficiente, die 2 ianuarii" fallback in the + calendarium itself -- 7-13 January can never be empty of a Sunday, so + Holy Family needs, and the calendarium carries, no such fallback). + [T.holy_family_sunday] is [named]'s own [week_origin Time_after_epiphany] + formula, restated for its own citation -- not re-derived a second time. + + Deliberately NOT part of [T.named]: [named] is checked before + [T.sunday_slug]'s own Christmastide branch in [T.temporal]'s dispatch, but + this feast's slug, rank and colour are EXACTLY what that branch already + computes for the 7-13 January Sunday (register's own pre-existing note on + [sunday_slug]: "the key stays lectio's") -- the only field that actually + needed to change is [subject] (silently always [Temporal] before this), + so [T.temporal] applies it as a targeted override inside that branch + rather than duplicating slug/rank/colour a second time through [named]'s + 4-tuple. *) +let test_holy_family () = + (* 2026: Epiphany (6 Jan) is a Tuesday, so Holy Family falls 11 January -- + an ordinary instance, no collision with the Baptism (13 Jan). *) + Alcotest.(check string) "2026: slug is the ordinary I-post-Epiphany Sunday key" + "ef-time-after-epiphany-sunday-1" (slug_of (d 2026 1 11)); + Alcotest.(check string) "2026: II class" "class-2" (rank_of (d 2026 1 11)); + Alcotest.(check string) "2026: white" "white" (colour_of (d 2026 1 11)); + Alcotest.(check bool) "2026: subject is Lord (RG 91 entry 14, \"Festa Domini II \ + classis\" -- the mystery of the Holy Family touches Christ)" + true + (subject_of (d 2026 1 11) = Sub.Lord); + (* 2019: Epiphany itself is a Sunday, so the first Sunday AFTER it is the + LATEST possible date, 13 January -- the one date Holy Family collides + with the fixed Commemoration of the Baptism of the Lord (data/ef/ + sanctoral.sexp: "commemoration-of-the-baptism-of-the-lord", also 13 + Jan, II class, subject Lord). [T.temporal]'s own resolution of WHO wins + that day is {!Rite_ef.Precedence_ef}'s business (band/disposition, see + test_precedence_ef.ml), not this function's -- this only proves + [T.temporal] itself still identifies 13 January 2019 as Holy Family's + own date, unconditionally, regardless of what else might compete for + the day once precedence is applied. *) + Alcotest.(check bool) "2019: 6 January is a Sunday" true (D.weekday (d 2019 1 6) = D.Sun); + Alcotest.(check string) "2019: Holy Family falls on the latest possible date, 13 Jan" + "2019-01-13" (D.to_iso8601 (T.holy_family_sunday 2019)); + Alcotest.(check string) "2019: still the same slug" "ef-time-after-epiphany-sunday-1" + (slug_of (T.holy_family_sunday 2019)); + Alcotest.(check bool) "2019: still subject Lord" true (subject_of (T.holy_family_sunday 2019) = Sub.Lord); + (* Every possible weekday for 6 January exercised, proving the formula + never leaves the 7-13 window empty (register's own citation above) -- + not merely the two years already checked above. *) + List.iter + (fun y -> + let hf = T.holy_family_sunday y in + Alcotest.(check bool) (Printf.sprintf "%d: Holy Family is a Sunday" y) true + (D.weekday hf = D.Sun); + Alcotest.(check bool) (Printf.sprintf "%d: Holy Family falls 7-13 January" y) true + (D.month hf = 1 && D.day hf >= 7 && D.day hf <= 13)) + [ 2019; 2020; 2021; 2022; 2023; 2024; 2025; 2026; 2027 ]; + (* Negative control: an ORDINARY Sunday after Epiphany (the 2nd) stays + [subject Temporal] -- the override is scoped to exactly one Sunday a + year, not every Time-after-Epiphany-shaped Sunday. *) + Alcotest.(check bool) "2026: the following Sunday (18 Jan, II after Epiphany) is NOT Lord" + true + (subject_of (d 2026 1 18) = Sub.Temporal) + +(* Three of [test_holy_family]'s own exhaustive-weekday sample, pinned to + their literal ISO dates and independently cross-checked against + `date -d +%A` (the same discipline test_golden.ml's own header + describes for its own weekday assertions) -- the earliest possible date + (Epiphany a Saturday), the latest (Epiphany a Sunday), and one ordinary + middle case, rather than trusting the structural "always 7-13" check + above on its own without ever anchoring it to real calendar dates. *) +let test_holy_family_no_gap_year () = + Alcotest.(check string) "2024 (6 Jan a Saturday): earliest possible date, 7 Jan" "2024-01-07" + (D.to_iso8601 (T.holy_family_sunday 2024)); + Alcotest.(check string) "2019 (6 Jan a Sunday): latest possible date, 13 Jan" "2019-01-13" + (D.to_iso8601 (T.holy_family_sunday 2019)); + Alcotest.(check string) "2025 (6 Jan a Monday): 12 Jan" "2025-01-12" + (D.to_iso8601 (T.holy_family_sunday 2025)) + (* Regression for a Task 14 Validate finding, reworked (register finding 4): [named] no longer carries its own week at all -- [temporal] computes it uniformly via [week] for every day, named or not -- so "a named day inside @@ -432,6 +514,36 @@ let test_anchors_erosion_is_caught () = [ "ef-ascension"; "ef-nativity" ] (missing_from_anchors ~named_slugs ~anchors:eroded_anchors) +(* Holy Family's own anchor coverage, kept SEPARATE from the two tests above: + it is genuinely NOT one of [T.named]'s outputs (test_holy_family's own + comment explains why -- [T.temporal] applies it as a targeted override + inside [T.sunday_slug]'s branch instead), so [named_slugs_for_year]'s scan + cannot see it, and folding it into [test_anchors_cover_all_named_days]/ + [test_anchors_erosion_is_caught] would misrepresent what those two are + actually proving ("everything [named] produces"). This proves the same + two properties directly against [T.holy_family_sunday] instead: the entry + is present, AND deleting it is caught -- {!Validate}'s own production + anchor check (validate.ml, register/spec §5.7) calls [temporal], not + [named], so it is unaffected by this distinction and already covers this + entry regardless; this is this file's own unit-level guard against the + same erosion. *) +let test_holy_family_anchor_present_and_erosion_is_caught () = + let y = 2026 in + let expected_slug = "ef-time-after-epiphany-sunday-1" in + let expected_date = T.holy_family_sunday y in + Alcotest.(check bool) "the anchor entry exists, at the right date" true + (List.mem (expected_slug, expected_date) (T.anchors y)); + (* Erosion: with the entry struck out, [T.temporal] itself still puts + [expected_slug] at [expected_date] (unaffected -- deleting an [anchors] + row never touches [temporal]'s own computation, only what is CHECKED + against it), so a reader who only trusted [anchors] would no longer be + told to look there at all. *) + let eroded = List.filter (fun (s, dt) -> not (String.equal s expected_slug && dt = expected_date)) (T.anchors y) in + Alcotest.(check bool) "the erosion is caught: the entry no longer appears" false + (List.mem (expected_slug, expected_date) eroded); + Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion" + expected_slug (slug_of expected_date) + let test_totality () = (* Every day of 2026 yields an office without raising. Not a slug re-validation -- Slug.t is a private string validated on construction, @@ -453,6 +565,8 @@ let suite_extra = Alcotest.test_case "nativity octave" `Quick test_nativity_octave; Alcotest.test_case "week numbers" `Quick test_week_numbers; Alcotest.test_case "sunday slugs" `Quick test_sunday_slugs; + Alcotest.test_case "holy family (RG 17(b))" `Quick test_holy_family; + Alcotest.test_case "holy family: no gap year (RG 17(b))" `Quick test_holy_family_no_gap_year; Alcotest.test_case "week/sunday_slug agree" `Quick test_week_sunday_slug_agree; Alcotest.test_case "resumed sundays" `Quick test_resumed_sundays; Alcotest.test_case "ferial slugs and ranks" `Quick test_ferial_slugs_and_ranks; @@ -465,6 +579,8 @@ let suite_extra = Alcotest.test_case "totality" `Quick test_totality; Alcotest.test_case "anchors cover all named days" `Quick test_anchors_cover_all_named_days; + Alcotest.test_case "holy family anchor present, erosion caught" `Quick + test_holy_family_anchor_present_and_erosion_is_caught; Alcotest.test_case "anchors erosion is caught" `Quick test_anchors_erosion_is_caught ] let suite = -- cgit v1.3 From 6319c8d5c012db40206b463dc912a1edf7aeec5d Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 08:15:48 +0200 Subject: precedence(ef): RG91 e14 "primum mobilia" + RG112(a) inseparable exclusion Two related fixes, both needed for Holy Family to actually observe correctly on the seven years it coincides with the fixed Commemoration of the Baptism of the Lord (13 January): 2008, 2013, 2019, 2030, 2036, 2041, 2047. band's entry 14 ("Festa Domini II classis") gated on sanctoral origin only, so a movable Class2 Lord feast had no row in the table at all. RG 91 entry 14's own text says otherwise: "primum mobilia, deinde fixa" -- movable first, then fixed. entry_14_movable_band gives Holy Family (the one movable candidate this codebase builds with subject Lord) a value strictly better than the fixed half's own literal 14, negative so it can never collide with a real table position -- without it, the two would tie at 14 and fall to Precedence.resolve's own alphabetical tie-break, which picks the WRONG side ("commemoration-of-the-baptism-of-the-lord" sorts before "ef-time-after-epiphany-sunday-1"). disposition gets a new branch for RG 112(a) (Caput XVI, "De Commemorationibus"): the Office, Mass or commemoration of a mystery of one Divine Person excludes a commemoration of another mystery of the SAME Divine Person. Winner and loser both subject Lord -> Omit, not Commemorate -- without it, Holy Family observing correctly still left the Baptism admitted as an ordinary Class2 commemoration under RG111(b), which the primary text does not allow. Confirmed by a MORE SPECIFIC primary source than RG 112(a) alone: the Holy Family Mass propers' own note, found on both photographic scans, word for word, immediately after the Postcommunio -- "Si festum S. Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S. Familiae, sine commemoratione Baptismatis D.N.I.C., et sine commemoratione dominicae." The electronic transcription carries neither this rubric nor RG 112(a)'s own worked example -- another instance of its documented defect, not merely absent from one check. Still expected red: the lectio differential (2005-2050) now shows a new, real divergence on those seven 13-January-Sunday dates -- lectio has no Holy Family at all, so it still shows the fixed Baptism observed there, unlike colitur now. The oracle's own M-series entries also still need widening for the observed-identity axis added two commits ago. Both are the next commits. --- lib/rites/rite_ef/precedence_ef.ml | 108 +++++++++++++++++++++++++++-- lib/rites/rite_ef/precedence_ef.mli | 17 +++++ test/test_precedence_ef.ml | 131 +++++++++++++++++++++++++++++++++++- 3 files changed, 248 insertions(+), 8 deletions(-) (limited to 'test') diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 750da8b..87cdeeb 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -48,6 +48,34 @@ let universal_layer = "ef-universal" let indult_prefix = "indult:" let unclassified = max_int +(* Not an RG citation -- an ordering device for RG 91 entry 14's own text + (scan-verified, docs/research/rules-register.md §4): "14. Festa Domini II + classis, PRIMUM MOBILIA, DEINDE FIXA" -- feasts of the Lord, II class, + MOVABLE FIRST, then fixed. Both halves are the SAME numbered table row, + but the primary text states a real priority between them, not merely two + disjoint categories -- [band]'s own entry-14 branches below must not + literally TIE at the shared integer 14, because a tie would fall to + {!Precedence.resolve}'s own kernel-level fallback ([compare_by], + Slug.compare), which is deliberately NOT rubric-authoritative (the exact + anti-pattern RG 113's own fix already corrected once for commemoration + ordering, this file's own header) and would in fact pick the WRONG side + here: "commemoration-of-the-baptism-of-the-lord" sorts before + "ef-time-after-epiphany-sunday-1" alphabetically, backwards from "primum + mobilia". Negative, so it can never collide with any real table position + (1..28) or [unclassified] (positive [max_int]) by construction -- not a + citation to some entry "13.5" that does not exist in the primary text, + just an ordering fact this integer has to carry because RG 91's own two + half-rows must compare unequal here. The one real witness today: Holy + Family (RG 17(b), temporal_ef.ml's own [holy_family_sunday]) against the + fixed Commemoration of the Baptism of the Lord (13 January) -- the only + date they can ever coincide, since Holy Family only ever falls 7-13 + January and no OTHER fixed Class2-Lord sanctoral entry shares that + window (register §6.0's own subject audit: Transfiguration 6 Aug, + Exaltation of the Cross 14 Sep, Dedication of the Archbasilica 9 Nov, + the Purification 2 Feb, the Baptism 13 Jan -- the Baptism alone falls in + Holy Family's own window). *) +let entry_14_movable_band = -14 + let is_indult layer = String.starts_with ~prefix:indult_prefix layer let is_universal layer = String.equal layer universal_layer @@ -223,13 +251,28 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc not the universal layer (11), and marked as an indult overlay (12's "not indult" test having just failed). *) else if (not is_temporal) && (not is_vigil) && rank = Class1 then 13 - (* 14: Feasts of the Lord, II class -- RG 91 entry 14, deliberately - UNQUALIFIED (contrast entry 16, which explicitly says "not - of the Lord"; RG 37c (§4, "Sundays") speaks of "II-class feasts of - the Lord" replacing an occurring II-class Sunday with no universal - qualifier either). No layer test here, unlike 11/12/13 and 16/19/20: - the register does not split this entry into universal/proper/indult, - so a proper or indult feast of the Lord still bands 14, not 19/20. *) + (* 14, MOVABLE half -- RG 91 entry 14's own "primum mobilia, deinde + fixa" ({!entry_14_movable_band}'s own comment has the full citation + and reasoning). Checked before the FIXED half immediately below, + matching the primary text's own order, though [is_temporal] already + makes the two branches structurally disjoint on any one candidate + regardless of which is checked first -- the ordering here is for a + reader following RG 91's own prose, not for correctness. The only + witness this codebase currently constructs is Holy Family + (temporal_ef.ml's own [holy_family_sunday] branch, the one place + [subject] is ever [Lord] on a temporal-origin candidate); a + hypothetical movable Holy-Name-of-Jesus office (RG 17(a), still + unbuilt as its own named day, register §6) would reach this same + branch too, once built, since nothing here is keyed to Holy Family's + own slug. *) + else if is_temporal && rank = Class2 && subject = Subject.Lord then entry_14_movable_band + (* 14, FIXED half -- RG 91 entry 14, deliberately UNQUALIFIED (contrast + entry 16, which explicitly says "not of the Lord"; RG 37c (§4, + "Sundays") speaks of "II-class feasts of the Lord" replacing an + occurring II-class Sunday with no universal qualifier either). No + layer test here, unlike 11/12/13 and 16/19/20: the register does not + split this entry into universal/proper/indult, so a proper or + indult feast of the Lord still bands 14, not 19/20. *) else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then 14 (* 15: Sundays, II class (every Sunday not already named at 6). *) else if is_temporal && rank = Class2 && is_sunday then entry_15_band @@ -732,6 +775,57 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) the RG26 branch above gives its own [not is_vigil] guard, rather than relying on that absence silently. *) Precedence.Omit + else if winner.Precedence.cel.Celebration.subject = Subject.Lord && cel.Celebration.subject = Subject.Lord + then + (* RG 112(a) (docs/research/rules-register.md §4/§6.0, primary text, + verified against BOTH photographic scans -- missale-romanum-1962.pdf + and "Missale Romanum 1962_text.pdf" -- the electronic transcription, + 1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum,_LT.pdf, carries + NEITHER this paragraph's own worked example NOR its own more + specific Mass-proper rubric below, exactly the transcription's + documented defect, this project's own methodology note): "112. Ad + commemorationes et orationes quod attinet, haec insuper serventur: + a) Officium, Missa aut commemoratio de aliquo festo vel mysterio + UNIUS DIVINAE PERSONAE excludit commemorationem aut orationem de + alio festo vel mysterio EIUSDEM DIVINAE PERSONAE" -- the Office, + Mass, or commemoration of some feast or MYSTERY of ONE Divine Person + excludes a commemoration or oration of ANOTHER feast or mystery of + the SAME Divine Person. + + {!Subject.t} has no finer split within [Lord] than "a mystery + touching the Divine Person of the Son" -- every real [subject = + Lord] entry this codebase's own data carries concerns Christ + specifically (register §6.0's subject audit: the Precious Blood, the + Transfiguration, the Exaltation of the Cross, the Dedication of the + Archbasilica, the Purification, the Baptism, and now Holy Family), + so reading "same Divine Person" as simply "both [Lord]" is safe at + today's data's own granularity -- a rite that ever needed to + distinguish, say, a Trinity-specific mystery from a Son-specific one + by Person would need a finer [Subject.t] first, not a special case + added here. + + The one live witness -- confirmed DIRECTLY, not only by this general + paragraph: the Holy Family Mass propers' own note, immediately + following the Postcommunion, word for word on BOTH photographic + scans: "Si festum S. Familiae occurrerit die 13 ianuarii, Missa + dicitur de festo S. Familiae, SINE COMMEMORATIONE BAPTISMATIS + D.N.I.C., et sine commemoratione dominicae" -- if the feast of the + Holy Family occurs on 13 January, the Mass is said of the Holy + Family, WITHOUT commemoration of the Baptism of Our Lord Jesus + Christ, and without commemoration of the Sunday (RG 17(b)'s own + general rule, restated for this specific date). Independently + confirmed against missalemeum (register §6.0): title "The Holy + Family: Jesus, Mary & Joseph", commemorations [], the Baptism + listed only under "displaced", never commemorated. + + Checked after RG 16(a) above (so a genuine RG 16(a) Sunday- + suppression is never re-explained under this citation instead) and + before the final ordinary-commemoration catch-all below, since + without it a [Feast]-status loser of this shape would otherwise + reach RG 95's ordinary "commemorated or omitted" branch and be + admitted as an ordinary Class2 commemoration under RG 111(b) -- + confirmed wrong against both primary sources above. *) + Precedence.Omit else (* RG 95's other branch: "aut commemorantur aut penitus omittuntur" -- commemorated or wholly omitted. Reached by every SANCTORAL loser diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index b283a17..86a0ead 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -62,6 +62,15 @@ val ember_prefixes : string list new rule, not a silently wrong one. *) val unclassified : int +(** RG 91 entry 14's own text (§4): "Festa Domini II classis, primum + mobilia, deinde fixa" -- feasts of the Lord, II class, MOVABLE first, + then fixed. {!band}'s value for the movable half (Holy Family, RG + 17(b)) -- strictly better than the fixed half's own literal [14], + negative so it can never collide with a real table position or + {!unclassified}. See precedence_ef.ml's own comment for the full + citation and why the two halves must not literally tie. *) +val entry_14_movable_band : int + (** [band ctx c]: RG 91's Table of Precedence. Returns the table's own entry number -- I class 1-13, II class 14-21, III class 22-26, IV class 27-28; lower wins (see {!Precedence.rules.band}) -- EXCEPT where the table's own @@ -134,6 +143,14 @@ val sunday_marker : string weekday, never a saint's day, and RG 111(c)/(d) admit an "ordinary" commemoration of a losing SAINT freely, with no such class-keyed gate); + - a loser whose [subject] is [Lord] AND whose WINNER's [subject] is also + [Lord] is [Omit] (RG 112(a) -- a feast or mystery of one Divine Person + excludes a commemoration of another feast or mystery of the SAME + Divine Person; live witness: Holy Family, RG 17(b), excluding the + fixed Commemoration of the Baptism of the Lord on the years 13 + January is itself Holy Family's own date -- see the .ml's own comment + for the primary text, including the Holy Family Mass propers' own + more specific 13-January rubric, on both photographic scans); - everything else -- including an impeded I-class Sunday, and a SANCTORAL loser of any rank below I class -- is [Commemorate], carrying its real RG 109 privilege (see {!admit} below); RG 109(a) diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 2935e40..01a54c0 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -138,6 +138,30 @@ let cases = cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:"diocese-warsaw" "ef-local-feast-of-the-lord", 14 ); + (* Entry 14, MOVABLE half -- RG 91 entry 14's own text (§4, scan-verified): + "Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- movable + FIRST, then fixed. Holy Family (RG 17(b), temporal_ef.ml's own + [holy_family_sunday]) is the one movable Class2 Lord feast this + codebase builds -- sourced from [Temporal_ef.temporal]'s own real + output ([of_temporal], the same review-finding-3 discipline entry + 18's own Ember row above uses), not a hand-typed slug, so a rename + or a dropped [~subject:Lord] on temporal_ef.ml's own side fails this + row too, not only test_temporal_ef.ml's. Must be a value STRICTLY + BETTER than the fixed half's own literal 14 immediately above (not + merely different) -- the two are structurally disjoint by [is_temporal] + alone so they can never literally collide on any one candidate, but + {!compare_by}'s own kernel-level tie-break (Slug.compare, deliberately + NOT rubric-authoritative -- the exact anti-pattern RG 113's own fix + already corrected once, this file's own header) is what would decide + between them if they returned the SAME band on the one date they can + coincide (13 January, when it is itself Holy Family's date) -- + "commemoration-of-the-baptism-of-the-lord" sorts before + "ef-time-after-epiphany-sunday-1" alphabetically, backwards from RG + 91's own stated order. See [PE.entry_14_movable_band]'s own comment + in precedence_ef.ml for why the value is negative rather than some + other distinct integer. *) + ( "14 Feast of the Lord, II class, MOVABLE half (Holy Family, RG 17(b))", + T.holy_family_sunday 2026, of_temporal (T.holy_family_sunday 2026), PE.entry_14_movable_band ); (* Entry 15 -- RG 91 entry 15 (§4): an ordinary Sunday not named at entry 6 -- Septuagesima is II class (RG 11-12 names only Advent/Lent/ Passiontide/Easter/Low/Pentecost as I class). *) @@ -662,7 +686,70 @@ let disposition_cases = cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-synthetic-class3-lord-feast", an_ordinary_sunday, - "Commemorate(Privileged)" ) + "Commemorate(Privileged)" ); + (* RG 112(a) (docs/research/rules-register.md §4/§6.0, primary text, + verified against BOTH photographic scans): "112. Ad commemorationes + et orationes quod attinet, haec insuper serventur: a) Officium, Missa + aut commemoratio de aliquo festo vel mysterio unius Divinae Personae + excludit commemorationem aut orationem de alio festo vel mysterio + eiusdem Divinae personae" -- the Office, Mass or commemoration of + some feast or MYSTERY of ONE Divine Person excludes a commemoration + or oration of ANOTHER feast or mystery of the SAME Divine Person. + Real witness, the ONE date these two candidates can ever coincide: + Holy Family (RG 17(b)) as winner, the Commemoration of the Baptism + of the Lord (13 January, data/ef/sanctoral.sexp: Class2, subject + Lord, status Feast) as loser, on the seven years 13 January is + itself Holy Family's own date (2008, 2013, 2019, 2030, 2036, 2041, + 2047). Directly confirmed by the Missal's own MORE SPECIFIC rubric, + found in the Holy Family Mass propers themselves (both photographic + scans, word for word): "Si festum S. Familiae occurrerit die 13 + ianuarii, Missa dicitur de festo S. Familiae, SINE COMMEMORATIONE + BAPTISMATIS D.N.I.C., et sine commemoratione dominicae" -- if the + feast of the Holy Family occurs on 13 January, the Mass is said of + the Holy Family, WITHOUT commemoration of the Baptism, and without + commemoration of the Sunday (RG 17(b)'s own general rule, restated). + The electronic transcription (1962-06-23,_SS_Ioannes_XXIII,_Missale + _Romanum,_LT.pdf) carries NEITHER sentence at all -- exactly the + transcription's own documented defect (this project's methodology + note), not merely absent from this one check. *) + ( "RG112(a): a feast/mystery of the Lord excludes a commemoration of \ + ANOTHER feast/mystery of the Lord -- Omit, not Commemorate (Holy \ + Family vs the Baptism of the Lord, 13 January)", + of_temporal (T.holy_family_sunday 2019), + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer + "commemoration-of-the-baptism-of-the-lord", + "Omit" ); + (* Conjunct 1/2, LOSER subject: an ORDINARY saint losing to the SAME + Lord-subject winner must still reach the ordinary commemorate flow, + not RG 112(a)'s exclusion -- RG 112(a) excludes another mystery of + the SAME Divine Person, not every commemoration a Lord feast happens + to outrank. Dropping the loser's own [subject = Lord] test would + wrongly Omit this ordinary saint's commemoration too. *) + ( "RG112(a) does NOT fire when only the WINNER is a mystery of the Lord \ + -- an ordinary saint stays Commemorate(Ordinary)", + of_temporal (T.holy_family_sunday 2019), + cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer + "ef-synthetic-ordinary-saint", + "Commemorate(Ordinary)" ); + (* Conjunct 2/2, WINNER subject: a Class2 SAINT'S feast (not of the + Lord) impeding an ordinary temporal candidate must not exclude an + otherwise-due Lord commemoration either -- RG 112(a) is keyed on the + WINNER's own subject too, not just the loser's. Real slug/rank from + data/ef/sanctoral.sexp (25 January, Class3, "Conversion of St + Paul") standing in for a same-rank saint winner; only the [subject] + distinction is under test here, not a real-date collision (Holy + Family's own subject is what makes this branch reachable at all -- + an ordinary sanctoral saint never wins against Holy Family in + practice, {!band}'s [entry_14_movable_band] sees to that, so this + row is SYNTHETIC in the same sense the RG16(a) block's own Class3 + row above is: defensive, not a live witness). *) + ( "SYNTHETIC: RG112(a) does NOT fire when only the LOSER is a mystery \ + of the Lord -- stays Commemorate(Ordinary)", + cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer + "conversion-of-st-paul", + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer + "commemoration-of-the-baptism-of-the-lord", + "Commemorate(Ordinary)" ) ] (* Task 9: [privilege_of]'s RG 109 categories (§4, "Commemorations"), @@ -1218,6 +1305,44 @@ let test_rg16a_lord_feast_suppresses_sunday_end_to_end () = (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted |> List.sort compare) +(* End-to-end proof of BOTH fixes this task adds together, {!P.resolve} in + full (band picks the winner, disposition and admit decide the loser's + fate), not just the two isolated functions [cases]/[disposition_cases] + already prove: 13 January 2019, the latest possible date Holy Family can + fall on (Epiphany itself a Sunday) and one of the seven years it + coincides with the fixed Commemoration of the Baptism of the Lord. + {!band}'s [entry_14_movable_band] (RG 91 entry 14, "primum mobilia") must + pick Holy Family as [observed], not the fixed Baptism candidate (which, + absent that fix, would win outright via {!compare_by}'s own alphabetical + tie-break at the shared literal 14 -- backwards from the primary text); + {!PE.disposition}'s new RG 112(a) branch must then Omit the Baptism + entirely, not merely demote it to a commemoration -- confirmed against + BOTH the general rubric (RG 112(a)) and the Holy Family Mass propers' own + more specific 13-January note (this file's own citation on the + RG112(a) [disposition_cases] rows above), and independently against + missalemeum's own JSON for this exact date (register §6.0): title "The + Holy Family: Jesus, Mary & Joseph", commemorations [], the Baptism + listed only under "displaced". *) +let test_holy_family_excludes_baptism_end_to_end () = + let date = T.holy_family_sunday 2019 in + Alcotest.(check string) "sanity: this really is 13 January 2019" "2019-01-13" (D.to_iso8601 date); + let day_ctx = ctx date in + let holy_family = of_temporal date in + let baptism = + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer + "commemoration-of-the-baptism-of-the-lord" + in + let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ baptism ] in + Alcotest.(check string) "Holy Family is observed, not the fixed Baptism (RG91 e14: movable first)" + "ef-time-after-epiphany-sunday-1" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check string) "II class" "class-2" (V.rank_to_string resolution.P.observed.P.cel.Cel.rank); + Alcotest.(check int) + "no commemorations at all -- RG112(a)'s own exclusion, not merely dropped by an admission count" + 0 (List.length resolution.P.commemorations); + Alcotest.(check (list string)) "the Baptism is OMITTED, not merely uncommemorated" + [ "commemoration-of-the-baptism-of-the-lord" ] + (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted) + (* Completes Task 7's carried fix (RG 91 entry 8, §4): on a real Sunday landing on 2 November, All Souls does not merely lose (that was Task 7's [band] fix, proved by [test_all_souls_yields_to_sunday] above) -- it must @@ -1465,6 +1590,10 @@ let suite = "RG16(a): a Feast of the Lord suppresses the Sunday's own commemoration entirely, \ end-to-end (2028-08-06)" `Quick test_rg16a_lord_feast_suppresses_sunday_end_to_end; + Alcotest.test_case + "RG112(a) + RG91 e14 \"primum mobilia\": Holy Family excludes the Baptism of the Lord \ + entirely, end-to-end (13 Jan 2019)" + `Quick test_holy_family_excludes_baptism_end_to_end; Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day" `Quick test_transfer_target_general_multi_step_search; Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday" -- cgit v1.3 From daca7de424be106c1cc5f3aa01950eda3b767a0f Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 08:17:36 +0200 Subject: test(differential): split the Holy Family/Baptism collision out of C1 (C15) Before this task's Holy Family fix, colitur's generic Sunday fallback lost outright to the fixed Commemoration of the Baptism of the Lord on 13 January whenever it fell on a Sunday, producing the SAME slug lectio's own tridentine-calendar.ini shows there -- squarely inside C1's own 6-13 January blanket (only Season differed). Now that Holy Family (RG 17(b)) correctly outranks the fixed Baptism and excludes it as a commemoration (RG 91 entry 14, RG 112(a)), colitur's own slug on those seven dates (2008, 2013, 2019, 2030, 2036, 2041, 2047) changes to ef-time-after-epiphany-sunday-1 -- a genuine identity divergence against lectio, which has no Holy Family at all. C1's own predicate does not absorb it (jan_6_13_slug has no entry for that slug), so it surfaced as unexplained, exactly the check the task brief asked for: split it out into its own cited entry rather than widening C1's predicate to swallow it silently. C1's own expected_rows moves 368 -> 361 (back to its pre-rebootstrap value, for a third and different reason than either of the first two moves recorded in its own note); the new C15 covers the 7 rows, gated on the literal date list AND colitur's own slug, the same identity guard C1/C6/C8/C14 already apply. --- data/ef/expected-divergences.sexp | 9 +++++++-- test/test_differential.ml | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/data/ef/expected-divergences.sexp b/data/ef/expected-divergences.sexp index a733273..8e104d9 100644 --- a/data/ef/expected-divergences.sexp +++ b/data/ef/expected-divergences.sexp @@ -45,8 +45,8 @@ ((id C1) (citation "RG 72-73 (Nativity/Epiphany season boundary, Jan 1-13) + RG 119a (white through \"expletum tempus Epiphaniae\"); register §3c item 1") (verdict colitur) - (note "6-13 January is Christmastide in colitur, matching RG 72-73's explicit boundary at 13 January (corroborated independently by RG 119a's colour rule, §3b). lectio's efSeason switches to time-after-epiphany on 6 January. The season disagreement cascades into colour (green vs white) and, on 7-8 January specifically, into the ferial slug family name (colitur's ef-christmas-2- vs lectio's ef-time-after-epiphany-1- for the same two days). REFRESHED (ef-rebootstrap fixture refresh, 2026-08-12): expected_rows moved 361 -> 368. NOT a new divergence -- the closed C13's own 7 rows (13 January in the 7 years it falls on a Sunday) fold back in here: lectio's fix wave independently fixed its own equivalent of the RG16(a) defect C13 used to allow-list, so on all 7 of those Sunday years lectio's slug now matches colitur's `commemoration-of-the-baptism-of-the-lord` too (not just rank/colour), leaving season as the ONLY residual difference on those rows -- squarely inside C1's own existing shape (diffs subset of [Season;Colour;Slug] with the Slug-diff escape clause unused). Re-derived independently (a Python cross-check reading colitur's own `day` CLI output for 2005-2050 and the refreshed lectio fixture directly, replicating diff_fields/layer_c_reason's own logic outside the OCaml comparator, then cross-checked against the OCaml comparator's own `test_layer_c_counts_match_citations` failure output before this entry's value was set -- not by running the comparator green and copying its number): 368.") - (expected_rows 368)) + (note "6-13 January is Christmastide in colitur, matching RG 72-73's explicit boundary at 13 January (corroborated independently by RG 119a's colour rule, §3b). lectio's efSeason switches to time-after-epiphany on 6 January. The season disagreement cascades into colour (green vs white) and, on 7-8 January specifically, into the ferial slug family name (colitur's ef-christmas-2- vs lectio's ef-time-after-epiphany-1- for the same two days). REFRESHED (ef-rebootstrap fixture refresh, 2026-08-12): expected_rows moved 361 -> 368. NOT a new divergence -- the closed C13's own 7 rows (13 January in the 7 years it falls on a Sunday) fold back in here: lectio's fix wave independently fixed its own equivalent of the RG16(a) defect C13 used to allow-list, so on all 7 of those Sunday years lectio's slug now matches colitur's `commemoration-of-the-baptism-of-the-lord` too (not just rank/colour), leaving season as the ONLY residual difference on those rows -- squarely inside C1's own existing shape (diffs subset of [Season;Colour;Slug] with the Slug-diff escape clause unused). REFRESHED AGAIN (ef-rg112-rg110 task, branch ef-rg112-rg110): expected_rows moved 368 -> 361, back to its PRE-ef-rebootstrap value, but for a THIRD, different reason -- SPLIT OUT to its own new entry, C15, not silently re-absorbed: colitur now BUILDS Holy Family (RG 17(b)) and, on these same 7 dates, correctly observes it in the fixed Baptism's place (RG 91 entry 14, RG 112(a)), so colitur's own slug there changes from `commemoration-of-the-baptism-of-the-lord` (which happened to already match lectio's own slug) to `ef-time-after-epiphany-sunday-1` (Holy Family's own, lectio has no equivalent) -- a genuine SLUG divergence now, no longer inside C1's own [Season]-only shape. See C15's own entry for the full account.") + (expected_rows 361)) ((id C6) (citation "RG 91 entry 17 (\"days within the Octave of the Nativity\")") (verdict colitur) @@ -62,3 +62,8 @@ (verdict colitur) (note "ef-rebootstrap fixture refresh (2026-08-12), replacing the closed C9's old 'Joseph on a Lent Sunday' shape: in 2008, 2035 and 2046, St Joseph's (19 March) RG 96 walk is congested enough by Passiontide/Holy Week/the Easter octave to cross Easter, landing on the Monday after Low Sunday -- exactly where the Annunciation (25 March), independently transferred under its own Attamen (a) exception, has already been assigned its \"sedes propria\". colitur's calendar.ml (place_transfers/compare_deferred) resolves the resulting same-band (RG 91 entry 11) tie between the two I-class feasts by slug order (\"annunciation...\" < \"joseph...\"), an engineering convention with no direct RG citation of its own (recorded honestly as such in calendar.ml's own comments and test_golden.ml's 2008 golden-pin comment) -- so the Annunciation keeps the Monday and Joseph's own walk continues one day further (1 April 2008; 3 April 2035 and 2046, since the Annunciation's own target there is 2 April), each a genuine free class-4 feria. lectio, by contrast, does NOT resolve this collision at all: its transferIfImpededEF walks each candidate independently with no awareness of a second contestant, so the Annunciation's own walk claims the Monday and Joseph is left with no transferred day anywhere -- merely tacked onto the Annunciation's own row as a trailing, never-admitted \"+joseph-spouse-of-the-bl-virgin-mary\" candidate (lectio's own commit 09a1444, \"calendar: record the RG 95 chained-transfer gap in the source\", states this outright: \"No functional change -- this collision is left unfixed, defensible against scope\"). RG 95 grants the right of translation to I-class feasts; Joseph has it and lectio's engine never exercises it for him here -- colitur giving him a real transferred day, somewhere, is the more textually faithful reading regardless of the harder sub-question below. OPEN SUB-QUESTION, recorded rather than silently resolved either way (docs/research/rules-register.md has the full account): whether Joseph, being impeded FIRST (19 March, six days before the Annunciation's own 25 March), should instead take priority for the Monday under RG 98's \"in paritate autem Officium prius impeditum praecedit\" (at equal table position, the office impeded first takes precedence) -- which would push the ANNUNCIATION to the later day instead. colitur's current slug-order tie-break was never checked against this specific reading; the Annunciation's own \"tamquam in sedem propriam\" wording is offered as the stronger textual argument for exempting it from an RG 97/98 queue contest altogether (it names a specific day as its own proper seat, not a provisional walk target), but this is this project's own reading of the primary text, not a settled point -- flagged as a genuine open item, not adjudicated with full confidence either direction. Independently re-derived (not copied from prose): `grep joseph-spouse-of-the-bl-virgin-mary` against both the refreshed fixture and colitur's own `day` CLI output for 2005-2050, filtered to rows with a real post-Layer-A/B diff -- exactly these 3 dates, cross-checked against the OCaml comparator's own failure output before this value was set.") (expected_rows 3)) + ((id C15) + (citation "RG 91 entry 14 (\"Festa Domini II classis, primum mobilia, deinde fixa\") + RG 17(b) (\"festum S. Familiae ... celebrandum dominica prima post Epiphaniam ... locum tenet dominicae occurrentis cum omnibus iuribus et privilegiis\") + RG 112(a) (\"Officium, Missa aut commemoratio de aliquo festo vel mysterio unius Divinae Personae excludit commemorationem ... de alio festo vel mysterio eiusdem Divinae personae\"), all scan-verified; register §6.0/§4") + (verdict colitur) + (note "ef-rg112-rg110 task: SPLIT OUT of C1's own blanket 6-13 January window, not a new kind of divergence -- the same discipline the now-closed C13 already established for this exact date (C1's own note above), applied again because building Holy Family reopened the identical shape C13 used to cover, for a different underlying reason. Before this task, colitur had no Holy Family office: on these seven dates (13 January when it is itself a Sunday -- 2008, 2013, 2019, 2030, 2036, 2041, 2047), colitur's generic Sunday fallback lost outright to the fixed Commemoration of the Baptism of the Lord (already subject Lord in the rebootstrapped data, RG16(a)'s existing mechanism), producing the SAME slug lectio's own tridentine-calendar.ini shows there (`commemoration-of-the-baptism-of-the-lord`) -- only Season differed, inside C1's own shape. Holy Family (RG 17(b)) now correctly outranks the fixed Baptism (RG 91 entry 14's own \"primum mobilia, deinde fixa\" -- {!Rite_ef.Precedence_ef.entry_14_movable_band}) and excludes it as a commemoration entirely (RG 112(a), directly confirmed by the Holy Family Mass propers' own more specific 13-January rubric, both photographic scans: \"Si festum S. Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S. Familiae, sine commemoratione Baptismatis D.N.I.C., et sine commemoratione dominicae\"), so colitur's own slug on these 7 dates changes to `ef-time-after-epiphany-sunday-1` (Holy Family's own -- unchanged from the plain Sunday key it always carried, temporal_ef.ml's own comment on why) -- a genuine SLUG divergence against lectio, which has no Holy Family at all and still shows the fixed Baptism observed. Gated on the literal 7-date list AND colitur's own slug (the same identity guard C1/C6/C8/C14 already apply, fix round 1's own finding 1). Derived from the OCaml comparator's own failure output directly (test_layer_c_counts_match_citations), not by hand-counting first and cross-checking after: 7.") + (expected_rows 7)) diff --git a/test/test_differential.ml b/test/test_differential.ml index 89e6873..94adb2c 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -471,6 +471,35 @@ let nativity_octave_day_slugs = note for the full account of why that was wrong). *) let joseph_annunciation_collision_dates_2005_2050 = [ "2008-04-01"; "2035-04-03"; "2046-04-03" ] +(* C15 -- ef-rg112-rg110 task: SPLIT OUT of C1's own blanket 6-13 January + window, not a new kind of divergence -- the same discipline C13 (now + closed, see the header above) already established for this exact date, + and the SAME check the task brief asked for explicitly ("if C1 now + absorbs a different observed celebration, split it out"). Before this + task, colitur had no Holy Family office at all: on these seven dates (13 + January, the one civil day it falls on a Sunday in the 2005-2050 window + -- 2008, 2013, 2019, 2030, 2036, 2041, 2047), colitur's own generic + Sunday fallback happened to lose outright to the fixed Commemoration of + the Baptism of the Lord (RG16(a)'s existing "festum Domini beats an + ordinary Sunday" mechanism, subject Lord already present in the + REBOOTSTRAPPED data), producing the SAME slug lectio's own tridentine- + calendar.ini shows for that date ("commemoration-of-the-baptism-of-the- + lord") -- only [Season] differed (RG 72-73 vs lectio's own 6 January + boundary), squarely inside C1's own shape. Now that Holy Family is built + (RG 17(b)) and correctly outranks the fixed Baptism (RG 91 entry 14, + "primum mobilia, deinde fixa"; RG 112(a) excludes the Baptism as a + commemoration too, both this task's own precedence_ef.ml changes), + colitur's own slug on these seven dates changes to + "ef-time-after-epiphany-sunday-1" (Holy Family's own, unchanged from the + ordinary-Sunday key it always carried, temporal_ef.ml's own comment on + why) -- a GENUINE identity divergence against lectio, which has no Holy + Family at all and still shows the fixed Baptism observed there. Gated on + the literal 7-date list AND colitur's own slug (the same guard C1/C6/C8/ + C14 already apply, fix round 1's own finding 1: a predicate must pin + WHICH celebration it is about, not only the date). *) +let holy_family_baptism_collision_dates_2005_2050 = + [ "2008-01-13"; "2013-01-13"; "2019-01-13"; "2030-01-13"; "2036-01-13"; "2041-01-13"; "2047-01-13" ] + (* [layer_c_reason l c diffs] returns the [data/ef/expected-divergences.sexp] [id] this row-pair's remaining (post Layer A/B) diff set belongs to, or [None] if nothing here explains it (a genuine, uncovered failure). *) @@ -516,6 +545,11 @@ let layer_c_reason (l : row) (c : row) diffs = every predicate -- geometry, not this pin. *) && String.equal c.slug "joseph-spouse-of-the-bl-virgin-mary" then Some "C14" + else if + List.mem l.date holy_family_baptism_collision_dates_2005_2050 + && subset diffs [ Season; Slug_f ] + && String.equal c.slug "ef-time-after-epiphany-sunday-1" + then Some "C15" else None (* ---------------------------------------------------------------------- *) -- cgit v1.3 From e5cafdd222a05032ae4f3c0ea07fe0eeda47c145 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 08:21:39 +0200 Subject: test(oracle): green -- widen five entries, add M18 for observed identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit layer_m_reason's own five entries whose diff shape was checked by EXACT list equality (M1, M2, M3, M13, M16) lost their match once the observed-identity axis widened every row's diff set two commits ago. Each is widened the same way commemoration-identity was already widened for M13 once before: the axis now ALSO fires alongside their existing citation, same root cause, same id -- M1/M3 (temporal-origin observed day, no name), M2 (same, plus five Paschaltide dates that used to match cleanly and now surface for the first time), M13 (Joseph, sanctoral, now a genuine title MISMATCH, not merely unresolved), M16 (the temporal Passiontide feria observed there). M18 covers the remainder: 373 of 730 days whose observed celebration is temporal-origin and so carries no English name at all -- the overwhelming majority of the window, including Holy Family's own two dates in it (2026-01-11, 2027-01-10), counted here rather than shown as a false Matched. Gated on the diff SHAPE alone (this file's own convention for a population this size, the same reasoning M2's own title-substring predicate already uses instead of a date list), verdict unresolvable -- the same fourth verdict M15 already established for the identical limit on the commemoration axis. Building an English name onto the temporal cycle is Plan 4's own scope, register §6. Teeth, per the brief: temporarily corrupted hilary's own English name in data/ef/sanctoral.sexp (14 January 2026, an OBSERVED sanctoral day, rank and colour left untouched) and reran. Reddened immediately: 2026-01-14: observed-identity-mismatch differ -- colitur=(rank=3 colour=w ...) oracle=(rank=3 colours=[w] title="St. Hilary" ...) rank and colour agree on both sides -- exactly the shape the pre-strengthening comparator would have slept through completely. Reverted (git diff on the data file is empty) before this commit. --- data/ef/expected-divergences-missalemeum.sexp | 22 ++++++- test/test_oracle.ml | 83 +++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 13 deletions(-) (limited to 'test') diff --git a/data/ef/expected-divergences-missalemeum.sexp b/data/ef/expected-divergences-missalemeum.sexp index 9cff236..d7a1c8c 100644 --- a/data/ef/expected-divergences-missalemeum.sexp +++ b/data/ef/expected-divergences-missalemeum.sexp @@ -35,6 +35,19 @@ ; primary-source effort, not defaulted past -- see each entry's own note ; and the task report for the full search. ; +; M18 -- ef-rg112-rg110 task (Holy Family, RG 17(b)): test_oracle.ml's own +; comparator was strengthened to compare the OBSERVED day's identity, not +; only its rank and colour (the gap that let the missing Holy Family go +; unnoticed by this layer for as long as it did -- rank 2/white on both +; sides purely by coincidence). [M18] is [verdict unresolvable], the SAME +; fourth verdict as M15, for the SAME reason: colitur's own resolved name is +; [None] for any TEMPORAL-origin observed day, the large majority of this +; window. M1, M2, M3, M13 and M16's own subsets were each widened to absorb +; this axis where it fires alongside their pre-existing citation (see each +; entry's own note) rather than being folded into M18's own count, which +; covers only rows where [Observed_identity_unresolved] is the SOLE +; disagreement. +; ; FIVE MORE REMOVED, ef-rebootstrap (2026-08-12), same shape as M17's own ; removal above -- not re-adjudicated to a different verdict, just gone, ; because the underlying divergence stopped occurring: M4 (second @@ -102,8 +115,8 @@ ((id M2) (citation "RG 91 entry 27 (\"Officium sanctae Mariae in sabbato\") -- register §4") (verdict missalemeum) - (note "Every otherwise-unoccupied IV-class Saturday should carry the votive Office of the BVM (white; missalemeum's own titles cycle \"I\"..\"V Mass of the B. V. M. -- Salve, Sancta Parens\"). precedence_ef.ml's [band] already has an entry-27 comment acknowledging this row exists (corrected attribution, final fix wave: this note previously said temporal_ef.ml, but [band] is precedence_ef.ml's own function), but [Temporal_ef.temporal] never actually CONSTRUCTS this office -- an unimpeded Time-after-Epiphany/-Pentecost Saturday gets a bare ferial slug and season green instead. A genuine feature gap, not a citation dispute; tracked in register §6, not built in this task.") - (expected_rows 17)) + (note "Every otherwise-unoccupied IV-class Saturday should carry the votive Office of the BVM (white; missalemeum's own titles cycle \"I\"..\"V Mass of the B. V. M. -- Salve, Sancta Parens\"). precedence_ef.ml's [band] already has an entry-27 comment acknowledging this row exists (corrected attribution, final fix wave: this note previously said temporal_ef.ml, but [band] is precedence_ef.ml's own function), but [Temporal_ef.temporal] never actually CONSTRUCTS this office -- an unimpeded Time-after-Epiphany/-Pentecost Saturday gets a bare ferial slug and season green instead. A genuine feature gap, not a citation dispute; tracked in register §6, not built in this task. REFRESHED (ef-rg112-rg110 task): expected_rows moved 17 -> 22, for the SAME reason M1/M3/M16 below move too -- the new observed-identity axis (test_oracle.ml's own header) now ALSO fires on 5 Paschaltide BVM-Saturday dates that used to match CLEANLY (colour already white there, RG 119, so the pre-existing [Colour_f]-only check never even reached these 5 rows; M2's own comment already noted \"every Paschaltide occurrence... never reaches this predicate at all\" -- no longer true once [Observed_identity_unresolved] alone is enough to reach it). Not a new divergence, the identical feature gap surfacing on a second axis -- derived from the comparator's own failure output (test_layer_m_counts_match_citations), not hand-counted.") + (expected_rows 22)) ((id M3) (citation "RG 87 (Minor Litanies/Rogations, Mon/Tue before Ascension) -- the SAME citation as the lectio allow-list's own C8 (data/ef/expected-divergences.sexp)") (verdict colitur) @@ -149,3 +162,8 @@ (verdict missalemeum) (note "27 March 2026 (Friday of Passion Week): colitur admits \"St. John Damascene\" (a real Class3 universal feast that genuinely falls that day, data/ef/sanctoral.sexp's own john-damascene entry, 27 March); missalemeum shows \"For Our Lady of the Seven Sorrows\" instead. The SAME standing gap M13 already names: the Seven-Sorrows-of-Passion-Friday commemoration is confirmed real in the primary text but not implemented anywhere in temporal_ef.ml, so colitur has no candidate for it and falls back to whichever ordinary Class3 saint actually wins that Friday. Verdict missalemeum: colitur is missing a real, primary-cited office, not differently opinioned. Register §6 open item (the same one M13's own note already tracks). Only 2026 shows here as an IDENTITY mismatch -- 2027's Friday of Passion Week IS 19 March, M13's own date, where identity comparison cannot even be reached (M13's own rank/colour mismatch already excludes that day from count-matched identity comparison, folded into M13's own widened subset above). NOTE for whoever builds the office (fix round 1, coordinator finding 7): 27 March 2026 is a III-class day, where RG 111(d) admits TWO commemorations -- yet missalemeum admits only the Seven Sorrows and DISPLACES John Damascene entirely, not merely drops him to second place. A naive implementation (one more ordinary III-class commemoration competing for the day's two slots) would give colitur two commemorations where missalemeum shows one; whatever privilege the Seven Sorrows carries must itself explain the exclusion, not just win admission.") (expected_rows 1)) + ((id M18) + (citation "test_oracle.ml's own header, \"COMMEMORATION IDENTITY\" section, restated for the OBSERVED axis (ef-rg112-rg110 task) -- not an RG citation, a comparator LIMIT: Rite_ef.Temporal_ef never sets an English [names] field on any candidate it builds, the SAME limit M15 already names for a commemoration, now visible for the OBSERVED day itself now that the observed-identity axis exists") + (verdict unresolvable) + (note "373 of the 730 days in this window -- the overwhelming majority of days whose observed celebration is TEMPORAL-origin (an ordinary Sunday, a feria, a movable named feast including Holy Family itself), which never carries an English name on colitur's side. Not a rubric dispute and not a data gap either engine is wrong about -- a LIMIT of this comparator, honestly counted rather than silently passed, per the brief's own explicit instruction (\"a day whose observed identity cannot be resolved must be a counted, allow-listed outcome, never a silent skip\"). Gated on the diff SHAPE alone (exactly, and only, observed-identity-unresolved), not a literal date list the way every other entry in this file is -- at this population size a date list would itself be the \"pattern that could silently widen\" this file's own header warns against, for the opposite reason a date RANGE is normally risky: the predicate (colitur's own resolved name is [None]) is the precise, falsifiable evidence, the same shape M2's own title-substring predicate already uses instead of enumerating dates, just keyed on presence-of-a-name rather than a title string. Building an English name onto every temporal-cycle candidate (a data/lectionary-bootstrap task, Plan 4, out of this task's scope -- register §6, the same open item M15's own note already tracks) would resolve this entry to Matched for every one of these days it does not instead expose as a REAL divergence (Holy Family's own two dates in this window, 2026-01-11 and 2027-01-10, are counted here, not separately -- the name comparison genuinely cannot distinguish \"Holy Family\" from any other unnamed Sunday, so proving Holy Family's own correctness rests on the golden pins and the differential's own C15, not this axis). Derived directly from the comparator's own failure output (test_layer_m_counts_match_citations), not hand-counted first and cross-checked after: 373.") + (expected_rows 373)) diff --git a/test/test_oracle.ml b/test/test_oracle.ml index ae5c396..7068af9 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -726,25 +726,86 @@ let m16_dates = [ "2026-03-27" ] entry needed at all -- removed, not re-adjudicated to a different verdict, since there is no longer a divergence to name. *) +(* M18 -- ef-rg112-rg110 task: the gap this file's own header now names under + "COMMEMORATION IDENTITY", restated for the OBSERVED axis. colitur's own + observed celebration carries an English name ({!Celebration.t}.names) + ONLY for a SANCTORAL-origin day (a saint's feast winning outright); a + TEMPORAL-origin one -- an ordinary Sunday, a feria, a movable named feast + including Holy Family itself -- never does ({!Rite_ef.Temporal_ef}'s own + [build] has no [en "..."] string anywhere in it, this file's own header + again). That is the OVERWHELMING majority of days in any calendar (saints' + feasts are common, but far from every day), so this is a single + STRUCTURAL predicate -- [Observed_identity_unresolved] alone, nothing + else disagreeing -- not a literal date list the way every other entry in + this file is: at 374 of the 730 days in this window, a list would be + exactly the "range/pattern that could silently widen" this file's own + header explicitly avoids elsewhere, for the OPPOSITE reason a range is + normally risky here -- the predicate itself (colitur's own name is + [None]) is the precise, falsifiable evidence, the same shape [M2]'s own + title-substring predicate already uses instead of a date list, just keyed + on presence-of-a-name rather than a title string. NOT a rubric dispute + and NOT a data gap either engine is wrong about -- a LIMIT of this + comparator itself, honestly counted rather than silently passed, per the + brief's own explicit instruction ("a day whose observed identity cannot + be resolved must be a counted, allow-listed outcome, never a silent + skip"), the SAME discipline [M15] already established for an unresolvable + COMMEMORATION, applied here for the first time to the OBSERVED day. Fixing + this at the root (giving every temporal-cycle candidate an English name) + is a data/lectionary-bootstrap task (Plan 4), not something this + comparator can do for itself -- register §6 tracks it, the same open item + [M15]'s own note already points to. *) let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = if diffs = [] then None - else if List.mem c.c_date m1_dates && subset diffs [ Rank; Colour_f; Comm_presence ] then Some "M1" - else if contains_substring o.o_title ~needle:bvm_saturday_title && diffs = [ Colour_f ] then Some "M2" - else if List.mem c.c_date m3_dates && diffs = [ Colour_f ] then Some "M3" + (* M1's own subset widened (this task): colitur's observed day on both + dates is TEMPORAL-origin (the Sunday itself, RG33's own point -- the + vigil should never have displaced it), so it now ALSO carries + [Observed_identity_unresolved] -- the same root cause the file's other + widened entries below share, restated for this one's own shape. *) + else if List.mem c.c_date m1_dates + && subset diffs [ Rank; Colour_f; Comm_presence; Observed_identity_unresolved ] + then Some "M1" + (* M2's own subset widened (this task): every BVM-Saturday date's observed + celebration is temporal-origin (the ordinary Saturday feria colitur + computes, not the distinct Office of the BVM missalemeum's title + names -- M2's own gap), so [Observed_identity_unresolved] now fires + alongside [Colour_f] where it used to fire alone, AND alone on its own + on every Paschaltide occurrence M2's own comment already says never + reached this predicate before (colour already agrees there; only the + new axis has anything left to say). *) + else if contains_substring o.o_title ~needle:bvm_saturday_title + && subset diffs [ Colour_f; Observed_identity_unresolved ] + then Some "M2" + (* M3's own subset widened (this task): the Rogation Monday feria colitur + observes is temporal-origin, same root cause as M1/M2 above. *) + else if List.mem c.c_date m3_dates && subset diffs [ Colour_f; Observed_identity_unresolved ] then + Some "M3" else if List.mem c.c_date m5_dates && diffs = [ Comm_presence ] then Some "M5" else if List.mem c.c_date m8_dates && diffs = [ Comm_presence ] then Some "M8" else if List.mem c.c_date m10_dates && diffs = [ Comm_presence ] then Some "M10" else if List.mem c.c_date m11_dates && diffs = [ Comm_presence ] then Some "M11" else if List.mem c.c_date m12_dates && diffs = [ Comm_count ] then Some "M12" - (* M13's own subset widened (Task B): the same underlying gap (the Seven - Sorrows commemoration is never constructed at all) now ALSO surfaces - as [Comm_identity_unresolved] on this date, since colitur's own - admitted commemoration there is temporal-origin (no English name) -- - one root cause, one id, both axes it touches. *) - else if List.mem c.c_date m13_dates && subset diffs [ Rank; Colour_f; Comm_identity_unresolved ] then - Some "M13" + (* M13's own subset widened AGAIN (this task, following the SAME pattern + Task B's own widening comment below records): colitur's observed + celebration here is Joseph, SANCTORAL-origin, so its name IS + resolvable -- and disagrees with missalemeum's own title outright + ([Observed_identity_mismatch], not [_unresolved]), the SAME underlying + gap (the Seven Sorrows commemoration is never constructed, so colitur + has no candidate to observe OR commemorate in its place) now visible + on a THIRD axis. *) + else if List.mem c.c_date m13_dates + && subset diffs [ Rank; Colour_f; Comm_identity_unresolved; Observed_identity_mismatch ] + then Some "M13" else if List.mem c.c_date m15_dates && diffs = [ Comm_identity_unresolved ] then Some "M15" - else if List.mem c.c_date m16_dates && diffs = [ Comm_identity_mismatch ] then Some "M16" + (* M16's own subset widened (this task): colitur's observed celebration + here is the temporal Passiontide feria (John Damascene is only a + COMMEMORATION on this Feast-status-losing day -- band picks the + temporal candidate outright at Class3 rank, register's own account of + this date), so [Observed_identity_unresolved] now fires alongside the + pre-existing [Comm_identity_mismatch]. *) + else if List.mem c.c_date m16_dates + && subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ] + then Some "M16" + else if diffs = [ Observed_identity_unresolved ] then Some "M18" else None (* ---------------------------------------------------------------------- *) -- cgit v1.3 From 8baabbd7e0b070197657d4d358ce5684dcda6255 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 08:22:55 +0200 Subject: test(golden): pin Holy Family in three shapes (RG 17(b)) An ordinary year (2026-01-11, no fixed-date competitor), the latest possible date and the one 13-January collision (2030-01-13, the fixed Baptism of the Lord excluded entirely by RG91 e14 + RG112(a)), and the earliest possible date (2024-01-07, Epiphany a Saturday) -- proving RG 17(b)'s own 7-13 January window at both ends, not only its middle. Every field independently traced against the primary text before typing it in, per this file's own header rule: season stays christmastide (RG 72-73, unaffected by which Sunday of it this is), week stays unset (Christmastide has no numbered weeks at all), slug is unchanged from the plain Sunday key this date always carried (temporal_ef.ml's own comment on why RG 17(b) needs no new lectionary key), rank II class, colour white. Each weekday independently cross-checked against "date -d +%A" (GNU coreutils). --- test/test_golden.ml | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) (limited to 'test') diff --git a/test/test_golden.ml b/test/test_golden.ml index 5292427..259b81a 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -552,6 +552,72 @@ let test_rg16a_lord_feast_no_sunday_commemoration_2028 () = colour=white comms=[] in=- out=[]" (describe d) +(* Holy Family (RG 17(b), scan-verified, docs/research/rules-register.md + §4/§6.0): "festum S. Familiae Iesu, Mariae, Ioseph, celebrandum dominica + prima post Epiphaniam" -- first Sunday after Epiphany, II class, EVERY + year, taking the Sunday's own place "cum omnibus iuribus et privilegiis" + (RG 17(b)'s own closing clause, the same formula RG 16(a) uses above). + Three shapes, each independently verified against the primary text + BEFORE being typed in here, per this file's own header rule: + + 1. AN ORDINARY YEAR (2026, 11 January): no fixed-date competitor in the + 7-13 January window besides St Hyginus (11 Jan, Class3, + Commemoration_only per data/ef/sanctoral.sexp) -- excluded from the + day's single Sunday slot outright by RG 111(b)'s own rank floor ("de + festo II classis"), the SAME mechanism test_precedence_ef.ml's own + "II-class Sunday admits only a de-festo-II-classis commemoration" pin + already proves for a different date -- so comms=[] is not itself new + ground, only Holy Family's own slug/rank/colour/season are. + `date -d 2026-01-11 +%A` = Sunday, independently confirmed. + 2. THE 13-JANUARY COLLISION (2030, the latest possible date -- Epiphany + itself a Sunday, 6 January): the fixed Commemoration of the Baptism + of the Lord (13 Jan, Class2, subject Lord, data/ef/sanctoral.sexp) + would otherwise contest the day directly, not merely lose a + commemoration slot -- RG 91 entry 14's own "primum mobilia, deinde + fixa" (precedence_ef.ml's own [entry_14_movable_band]) is what keeps + Holy Family observed, and RG 112(a) -- directly confirmed by the Holy + Family Mass propers' own more specific 13-January rubric, both + photographic scans, word for word: "Si festum S. Familiae occurrerit + die 13 ianuarii, Missa dicitur de festo S. Familiae, sine + commemoratione Baptismatis D.N.I.C., et sine commemoratione + dominicae" -- is what keeps comms=[] rather than admitting the + Baptism as an ordinary Class2 commemoration. `date -d 2030-01-13 + +%A` = Sunday, independently confirmed; 2030 is one of the seven + years in the 2005-2050 window this collision occurs (register §6.0's + own list, test_differential.ml's own C15). + 3. THE EARLIEST POSSIBLE DATE (2024, 7 January -- Epiphany a Saturday): + the OTHER end of RG 17(b)'s own 7-13 January range, proving the + window's own boundary, not only its middle and its latest instance. + No fixed-date competitor exists this early in the window either. + `date -d 2024-01-07 +%A` = Sunday, independently confirmed. + + All three: season=christmastide (RG 72-73's own 1-13 January boundary, + unaffected by Holy Family), week=- (Christmastide carries no numbered + weeks at all, [T.week_origin]'s own [None] case -- unaffected by which + Sunday of it this is), slug=ef-time-after-epiphany-sunday-1 (unchanged + from the plain Sunday key this date always carried -- temporal_ef.ml's + own comment on why RG 17(b) does not need a new lectionary key), rank + =class-2, colour=white. *) +let test_holy_family_ordinary_year_2026 () = + check ~msg:"2026-01-11: Holy Family, ordinary year -- Hyginus (Class3, Commemoration_only) has no \ + standing for the day's II-class-only Sunday slot" + 2026 1 11 + "2026-01-11 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \ + colour=white comms=[] in=- out=[]" + +let test_holy_family_excludes_baptism_2030 () = + check ~msg:"2030-01-13: Holy Family on the latest possible date -- RG91 e14 \"primum mobilia\" keeps it \ + observed over the fixed Baptism, RG112(a) excludes the Baptism entirely, not merely demotes it" + 2030 1 13 + "2030-01-13 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \ + colour=white comms=[] in=- out=[]" + +let test_holy_family_earliest_possible_date_2024 () = + check ~msg:"2024-01-07: Holy Family on the earliest possible date (Epiphany a Saturday)" + 2024 1 7 + "2024-01-07 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \ + colour=white comms=[] 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). @@ -704,6 +770,13 @@ let suite = Alcotest.test_case "RG16(a), the Purification (oracle-ruled): observed outright, the Sunday not commemorated (2025-02-02)" `Quick test_purification_on_a_sunday_2025; + Alcotest.test_case "Holy Family (RG17(b)), ordinary year (2026-01-11)" `Quick + test_holy_family_ordinary_year_2026; + Alcotest.test_case + "Holy Family excludes the Baptism entirely on the latest possible date (RG91 e14 + RG112(a), 2030-01-13)" + `Quick test_holy_family_excludes_baptism_2030; + Alcotest.test_case "Holy Family, earliest possible date (2024-01-07)" `Quick + test_holy_family_earliest_possible_date_2024; 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)" -- cgit v1.3 From b23020c7acc6b22d99ffeeac5d65e9ad4a58c82c Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 09:11:21 +0200 Subject: precedence(ef): fix round 1 (F1) -- band's -14 sentinel inverted the table entry_14_movable_band = -14 was justified only by "cannot collide with a real table position (1..28) or unclassified" -- a collision argument, not an ordering one. band's own value is compared by plain <, so a negative number beats every I-class entry (1-13), not merely the fixed Baptism it was built to beat. Not live against the shipped universal sanctoral data (no I-class universal feast falls 7-13 January, which is why the original 1583-9999 sweep showed only the intended shape), but entries 12/13 admit PROPER and INDULT I-class feasts too, and those arrive via adjustments.sexp-style overlays -- this architecture's own advertised extension path. A diocesan patronal feast landing in Holy Family's window would have made the inversion live immediately: the I-class feast displaced and transferred to the next day, reproduced and confirmed before this fix, reverted after. Fixed by rescaling every band branch to the real RG 91 entry number times ten (unclassified untouched at max_int) instead of the bare 1..28, reserving genuine headroom immediately before each entry for a movable sub-rank -- not an arbitrary rescale: RG 91's own text records a "primum mobilia, deinde fixa" split at three rows, not only entry 14 (entry 12's own ninth sub-item, entry 14, and entry 20), so this leaves room for the other two without a second rescale if either ever gets a live witness. entry_14_movable_band now derives from entry_14_fixed_band - 1, tied together so they cannot drift apart. Added a synthetic I-class end-to-end test reproducing the exact counter-example that found this: a universal I-class candidate inside Holy Family's own 7-13 January window is now observed and does not transfer, with Holy Family itself falling to RG109(a)'s ordinary privileged-Sunday-commemoration path, the same treatment any genuine feast outranking an ordinary Sunday gets. --- lib/rites/rite_ef/precedence_ef.ml | 149 +++++++++++++++-------- lib/rites/rite_ef/precedence_ef.mli | 41 +++++-- test/test_precedence_ef.ml | 232 +++++++++++++++++++++++------------- 3 files changed, 276 insertions(+), 146 deletions(-) (limited to 'test') diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 87cdeeb..39251a7 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -48,33 +48,72 @@ let universal_layer = "ef-universal" let indult_prefix = "indult:" let unclassified = max_int -(* Not an RG citation -- an ordering device for RG 91 entry 14's own text - (scan-verified, docs/research/rules-register.md §4): "14. Festa Domini II - classis, PRIMUM MOBILIA, DEINDE FIXA" -- feasts of the Lord, II class, - MOVABLE FIRST, then fixed. Both halves are the SAME numbered table row, - but the primary text states a real priority between them, not merely two - disjoint categories -- [band]'s own entry-14 branches below must not - literally TIE at the shared integer 14, because a tie would fall to +(* CORRECTED, fix round 1 (coordinator finding 1) -- the ORIGINAL version of + this constant was [-14], justified only by "cannot collide with a real + table position (1..28) or [unclassified]". That is a COLLISION argument, + not an ORDERING one, and [band]'s own return value is compared by + {!Precedence.compare_by}/{!compare_precedence} as a plain [<] -- a + negative value is not merely "distinct from 1..28", it is LOWER than + every one of them, so a movable Class2 Lord feast (Holy Family) would + have beaten every I-class day (entries 1-13) outright, not merely the + fixed Baptism it was built to beat. Not live against the shipped + UNIVERSAL sanctoral data (no I-class universal feast falls 7-13 January), + which is why the original 1583-9999 blast radius (§6.2) showed only the + one intended shape -- but entries 12/13 admit PROPER and INDULT I-class + feasts too (patron/titular/dedication days, {!is_universal}/{!is_indult} + below), which arrive via `data/ef/adjustments.sexp`-style overlays, this + architecture's own advertised extension point -- a diocesan I-class + patronal feast landing 7-13 January would have made the inversion live + immediately, transferring the I-class feast as though a II-class Sunday + had impeded it. Demonstrated directly (fix round 1, reproduced then + reverted): a synthetic Class1 sanctoral candidate on 11 January was + OBSERVED as Holy Family (band -14) and the synthetic candidate itself + TRANSFERRED to the next day -- backwards on every count. + + FIXED: every {!band} branch now returns the real RG 91 entry number + TIMES TEN (entry 1 -> 10, entry 28 -> 280, {!unclassified} untouched at + [max_int]) instead of the bare 1..28 -- not an arbitrary rescale, a + deliberate one: RG 91's own text records a "primum mobilia, deinde + fixa" (movable-then-fixed) split at THREE rows, not only this one -- + entry 12's own ninth sub-item ("Festa indulta I classis, primum + mobilia, deinde fixa"), entry 14 (this one), and entry 20 ("indult + II-class feasts (movable then fixed)", register §4's own summary) -- + and only entry 14 has a live witness in this codebase's data today. The + ×10 scale reserves nine integers of genuine headroom immediately BEFORE + each real entry's own value for exactly this kind of sub-rank, without + requiring a second rescale if entry 12's or entry 20's own movable half + ever needs one too -- a value DERIVABLE from RG 91's own table structure, + not an arithmetic convenience chosen to dodge one collision. [band]'s own + entry-14 comments (below) restate this scale at each branch it touches. *) +let entry_14_fixed_band = 140 + +(* RG 91 entry 14's own text (scan-verified, docs/research/rules-register.md + §4): "14. Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- feasts + of the Lord, II class, MOVABLE FIRST, then fixed. Both halves are the + SAME numbered table row, but the primary text states a real priority + between them, not merely two disjoint categories -- [band]'s own + entry-14 branches below must not literally TIE at + {!entry_14_fixed_band}, because a tie would fall to {!Precedence.resolve}'s own kernel-level fallback ([compare_by], Slug.compare), which is deliberately NOT rubric-authoritative (the exact anti-pattern RG 113's own fix already corrected once for commemoration ordering, this file's own header) and would in fact pick the WRONG side here: "commemoration-of-the-baptism-of-the-lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically, backwards from "primum - mobilia". Negative, so it can never collide with any real table position - (1..28) or [unclassified] (positive [max_int]) by construction -- not a - citation to some entry "13.5" that does not exist in the primary text, - just an ordering fact this integer has to carry because RG 91's own two - half-rows must compare unequal here. The one real witness today: Holy - Family (RG 17(b), temporal_ef.ml's own [holy_family_sunday]) against the - fixed Commemoration of the Baptism of the Lord (13 January) -- the only - date they can ever coincide, since Holy Family only ever falls 7-13 - January and no OTHER fixed Class2-Lord sanctoral entry shares that - window (register §6.0's own subject audit: Transfiguration 6 Aug, - Exaltation of the Cross 14 Sep, Dedication of the Archbasilica 9 Nov, - the Purification 2 Feb, the Baptism 13 Jan -- the Baptism alone falls in - Holy Family's own window). *) -let entry_14_movable_band = -14 + mobilia". Derived from {!entry_14_fixed_band}, one better (lower) than + it, using the one integer of headroom the ×10 scale reserves immediately + before every real entry -- not a citation to some entry "13.5" that does + not exist in the primary text, just the ordering fact RG 91's own two + half-rows require, expressed the same way the whole table now is. The + one real witness today: Holy Family (RG 17(b), temporal_ef.ml's own + [holy_family_sunday]) against the fixed Commemoration of the Baptism of + the Lord (13 January) -- the only date they can ever coincide, since + Holy Family only ever falls 7-13 January and no OTHER fixed Class2-Lord + sanctoral entry shares that window (register §6.0's own subject audit: + Transfiguration 6 Aug, Exaltation of the Cross 14 Sep, Dedication of the + Archbasilica 9 Nov, the Purification 2 Feb, the Baptism 13 Jan -- the + Baptism alone falls in Holy Family's own window). *) +let entry_14_movable_band = entry_14_fixed_band - 1 let is_indult layer = String.starts_with ~prefix:indult_prefix layer let is_universal layer = String.equal layer universal_layer @@ -155,8 +194,12 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc (* Named so entry 8's Sunday exception below can read "one worse than the Sunday it must yield to" rather than a bare integer that happens to equal entry 15's own value; entry 15's own branch returns this same - binding, not a second literal, so the two can never drift apart. *) - let entry_15_band = 15 in + binding, not a second literal, so the two can never drift apart. + CORRECTED, fix round 1 (coordinator finding 1): every branch below now + returns the real RG 91 entry number TIMES TEN, not the bare 1..28 -- + see {!entry_14_fixed_band}'s own comment for why (a genuine ordering + bug the bare scale could not express, not a cosmetic rename). *) + let entry_15_band = 150 in let open Vocab_ef in (* CORRECTED (Task B fix round 1, ef-rg16a): a [Commemoration_only] celebration has NO row in RG 91's table at all, checked FIRST, ahead of @@ -199,9 +242,9 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc ea22ad2. The guard is right; its former justification was not. *) if status = Celebration.Commemoration_only then unclassified (* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *) - else if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 1 + else if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 10 (* 2: Sacred Triduum (Thu-Sat of Holy Week). *) - else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 2 + else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 20 (* 3: Epiphany, Ascension, Holy Trinity, Corpus Christi, Sacred Heart, Christ the King. *) else if is_temporal && rank = Class1 @@ -209,25 +252,25 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc || off = 39 (* Ascension *) || off = 56 (* Trinity *) || off = 60 (* Corpus Christi *) || off = 68 (* Sacred Heart *) || Date.compare date (Temporal_ef.christ_the_king (Date.year date)) = 0) - then 3 + then 30 (* 4: Immaculate Conception, Assumption BVM. *) - else if (not is_temporal) && rank = Class1 && ((m = 12 && d = 8) || (m = 8 && d = 15)) then 4 + else if (not is_temporal) && rank = Class1 && ((m = 12 && d = 8) || (m = 8 && d = 15)) then 40 (* 5: Vigil & Octave day of the Nativity. *) - else if is_temporal && rank = Class1 && ((m = 12 && d = 24) || (m = 1 && d = 1)) then 5 + else if is_temporal && rank = Class1 && ((m = 12 && d = 24) || (m = 1 && d = 1)) then 50 (* 6: Sundays of Advent, Lent, Passiontide, and Low Sunday. *) else if is_temporal && rank = Class1 && is_sunday && (season = Advent || season = Lent || season = Passiontide || off = 7) - then 6 + then 60 (* 7: I-class ferias not above -- Ash Wednesday; Mon/Tue/Wed of Holy Week. Thu-Sat of Holy Week are the Triduum, entry 2 above, not this entry. *) - else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 7 + else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 70 (* 8: All Souls -- RG 91 entry 8's own text (§4) carries a qualifier this transcription must honour: "yields to an occurring Sunday". 2 November is always Time_after_pentecost (well clear of Advent/Lent/Passiontide and of every other entry's own Easter-relative or fixed date), so a Sunday landing on it is always an ordinary entry-15 II-class Sunday -- the one and only rival this exception ever has to lose to. On such a - Sunday this returns [entry_15_band + 1]: strictly worse than 15 (an + Sunday this returns [entry_15_band + 1]: strictly worse than 150 (an exact tie would fall to Precedence.resolve's slug tie-break, which for "ef-all-souls" against a "ef-time-after-pentecost-sunday-*" slug would make All Souls WIN -- the precise bug this guards against), but @@ -237,20 +280,20 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc feasts transfer) still sees the true I-class candidate it needs to move to 3 November. *) else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then - if is_sunday then entry_15_band + 1 else 8 + if is_sunday then entry_15_band + 1 else 80 (* 9: Vigil of Pentecost. *) - else if is_temporal && rank = Class1 && off = 48 then 9 + else if is_temporal && rank = Class1 && off = 48 then 90 (* 10: Days within the Octaves of Easter and Pentecost. *) else if is_temporal && rank = Class1 && ((off >= 1 && off <= 6) || (off >= 50 && off <= 55)) - then 10 + then 100 (* 11: I-class feasts of the universal Church not above. *) - else if (not is_temporal) && (not is_vigil) && rank = Class1 && is_universal layer then 11 + else if (not is_temporal) && (not is_vigil) && rank = Class1 && is_universal layer then 110 (* 12: Proper I-class feasts. *) - else if (not is_temporal) && (not is_vigil) && rank = Class1 && not (is_indult layer) then 12 + else if (not is_temporal) && (not is_vigil) && rank = Class1 && not (is_indult layer) then 120 (* 13: Indult I-class feasts. By elimination once 11 and 12 have failed: not the universal layer (11), and marked as an indult overlay (12's "not indult" test having just failed). *) - else if (not is_temporal) && (not is_vigil) && rank = Class1 then 13 + else if (not is_temporal) && (not is_vigil) && rank = Class1 then 130 (* 14, MOVABLE half -- RG 91 entry 14's own "primum mobilia, deinde fixa" ({!entry_14_movable_band}'s own comment has the full citation and reasoning). Checked before the FIXED half immediately below, @@ -272,30 +315,32 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc occurring II-class Sunday with no universal qualifier either). No layer test here, unlike 11/12/13 and 16/19/20: the register does not split this entry into universal/proper/indult, so a proper or - indult feast of the Lord still bands 14, not 19/20. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then 14 + indult feast of the Lord still bands {!entry_14_fixed_band}, not + 19/20. *) + else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then + entry_14_fixed_band (* 15: Sundays, II class (every Sunday not already named at 6). *) else if is_temporal && rank = Class2 && is_sunday then entry_15_band (* 16: II-class feasts of the universal Church, not of the Lord. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 16 + else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 160 (* 17: Days within the Octave of the Nativity (26-28 Dec are Stephen, John, the Innocents -- sanctoral, not this entry). *) - else if is_temporal && rank = Class2 && m = 12 && (d = 29 || d = 30 || d = 31) then 17 + else if is_temporal && rank = Class2 && m = 12 && (d = 29 || d = 30 || d = 31) then 170 (* 18: II-class ferias -- Advent 17-23 Dec; Ember days of Advent, Lent, September. *) else if is_temporal && rank = Class2 && ((season = Advent && m = 12 && d >= 17 && d <= 23) || is_ember_18 slug) - then 18 + then 180 (* 19: Proper II-class feasts. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 && not (is_indult layer) then 19 + else if (not is_temporal) && (not is_vigil) && rank = Class2 && not (is_indult layer) then 190 (* 20: Indult II-class feasts. By elimination, as at 13. *) - else if (not is_temporal) && (not is_vigil) && rank = Class2 then 20 + else if (not is_temporal) && (not is_vigil) && rank = Class2 then 200 (* 21: II-class vigils (Ascension, Assumption, John Baptist, Peter & Paul -- can be temporal- or sanctoral-origin, see the file comment above). *) - else if rank = Class2 && is_vigil then 21 + else if rank = Class2 && is_vigil then 210 (* 22: Ferias of Lent and Passiontide (Thursday after Ash Wednesday to the Saturday before Palm Sunday), except the Ember days (18 above). *) - else if is_temporal && rank = Class3 && (season = Lent || season = Passiontide) then 22 + else if is_temporal && rank = Class3 && (season = Lent || season = Passiontide) then 220 (* 23: III-class feasts in particular calendars. Unlike 11/12 and 14/16 above, the universal entry (24) is the HIGHER number here -- RG 91's own table ranks a particular-calendar III-class feast ahead of a @@ -303,13 +348,13 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc the register states it, not "corrected" into the other classes' pattern. RG 91 has no indult sub-rank at III class, so every non-base layer lands here, not split further. *) - else if (not is_temporal) && (not is_vigil) && rank = Class3 && not (is_universal layer) then 23 + else if (not is_temporal) && (not is_vigil) && rank = Class3 && not (is_universal layer) then 230 (* 24: III-class feasts in the universal calendar. *) - else if (not is_temporal) && (not is_vigil) && rank = Class3 then 24 + else if (not is_temporal) && (not is_vigil) && rank = Class3 then 240 (* 25: Ferias of Advent to 16 Dec, except the Ember days (18 above). *) - else if is_temporal && rank = Class3 && season = Advent then 25 + else if is_temporal && rank = Class3 && season = Advent then 250 (* 26: III-class vigils (St Lawrence). *) - else if rank = Class3 && is_vigil then 26 + else if rank = Class3 && is_vigil then 260 (* 27: Office of the BVM on Saturday -- every otherwise-unoccupied IV-class Saturday, per the historical default that fills it; ordinary Mass propers still make Rogation Mon/Tue/Wed proper without changing the @@ -318,12 +363,12 @@ let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedenc vigils for the same reason 11-13/14/16/19/20/23/24 do: RG 91 has no IV-class vigil at all (RG 91's own vigil list, §4 "Vigils", stops at III class), so one would be an anomaly, not this entry. *) - else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 27 + else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 270 (* 28: IV-class ferias -- the unqualified catch-all (temporal_ef.ml's own comment on [ferial_rank] cites the same primary text, "Feriae IV classis"). Excludes vigils for the same reason as 27 above: a IV-class "feria" that is also a vigil is not a feria RG 91 describes. *) - else if (not is_vigil) && rank = Class4 then 28 + else if (not is_vigil) && rank = Class4 then 280 else unclassified (* Task 8: what happens to the day's LOSING candidate (docs/research/ diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 86a0ead..239a254 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -62,25 +62,40 @@ val ember_prefixes : string list new rule, not a silently wrong one. *) val unclassified : int +(** {!band}'s value for RG 91 entry 14's FIXED half (a sanctoral, i.e. + fixed-date, Class2 feast of the Lord) -- see {!entry_14_movable_band}'s + own comment for why entry 14 needs two distinct values at all. *) +val entry_14_fixed_band : int + (** RG 91 entry 14's own text (§4): "Festa Domini II classis, primum mobilia, deinde fixa" -- feasts of the Lord, II class, MOVABLE first, then fixed. {!band}'s value for the movable half (Holy Family, RG - 17(b)) -- strictly better than the fixed half's own literal [14], - negative so it can never collide with a real table position or - {!unclassified}. See precedence_ef.ml's own comment for the full - citation and why the two halves must not literally tie. *) + 17(b)) -- [entry_14_fixed_band - 1], strictly better than the fixed + half without colliding with entry 13 or any other real table position. + CORRECTED, fix round 1 (coordinator finding 1): this used to be a + negative sentinel, justified only by "cannot collide with a real table + position" -- a COLLISION argument, not an ORDERING one; since {!band} + values are compared by plain [<], a negative value beats every I-class + entry (1-13) as well as the fixed half, not merely the fixed half it + was built to beat. {!band}'s own entries now use the real RG 91 number + TIMES TEN throughout, reserving genuine headroom before every entry -- + see precedence_ef.ml's own comment for the full citation, the counter- + example that found this, and why entries 12 and 20 may need the same + treatment if their own "movable then fixed" halves ever get a witness. *) val entry_14_movable_band : int (** [band ctx c]: RG 91's Table of Precedence. Returns the table's own entry - number -- I class 1-13, II class 14-21, III class 22-26, IV class 27-28; - lower wins (see {!Precedence.rules.band}) -- EXCEPT where the table's own - text states an exception: entry 8 (All Souls) reads "yields to an - occurring Sunday", so on a Sunday this returns a value that - loses to entry 15 rather than the literal integer 8 (see the comment on - entry 8 in precedence_ef.ml for the exact value and why). ALSO - {!unclassified} for any [Cel.Commemoration_only] candidate, checked - first, ahead of every rank-keyed branch -- CORRECTED, Task B fix round - 1 (coordinator finding 1): RG 91's own table enumerates only "dies + number TIMES TEN -- I class 10-130, II class 140-210, III class 220-260, + IV class 270-280; lower wins (see {!Precedence.rules.band}) -- EXCEPT + where the table's own text states an exception: entry 8 (All Souls) + reads "yields to an occurring Sunday", so on a Sunday this returns a + value that loses to entry 15 (150) rather than 80 (see the comment on + entry 8 in precedence_ef.ml for the exact value and why); entry 14 + (Feasts of the Lord, II class) splits into {!entry_14_movable_band} and + {!entry_14_fixed_band} per the table's own "primum mobilia, deinde + fixa". ALSO {!unclassified} for any [Cel.Commemoration_only] candidate, + checked first, ahead of every rank-keyed branch -- CORRECTED, Task B fix + round 1 (coordinator finding 1): RG 91's own table enumerates only "dies liturgici" (entry 24's own text, "Festa III classis..." -- FEASTS), so a celebration reduced to a bare commemoration was never a row in the table to begin with, confirmed by the calendarium's own notation (a diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 01a54c0..98ae0c8 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -48,96 +48,121 @@ let easter = Comp.gregorian_easter 2026 let off n = D.add_days easter n (* (description, date, candidate, expected RG 91 entry). *) +(* CORRECTED, fix round 1 (coordinator finding 1): every expected [int] + below is now the real RG 91 entry number TIMES TEN (entry 1 -> 10, entry + 28 -> 280), matching [PE.band]'s own rescale -- see precedence_ef.ml's + own [entry_14_fixed_band] comment for why the bare 1..28 scale was a real + ordering bug, not a style choice. The description strings still say the + bare entry number ("14 Feast of the Lord...") -- that names the RG 91 + ROW, not the band integer, and is unaffected by the scale. *) let cases = [ (* Entry 1 -- RG 91 entry 1 (§4): Nativity, Easter Sunday, Pentecost Sunday. *) - ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 1); - ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 1); - ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 1); + ("1 Nativity", mk 2026 12 25, cand "ef-nativity", 10); + ("1 Easter Sunday", off 0, cand "ef-easter-sunday", 10); + ("1 Pentecost Sunday", off 49, cand "ef-pentecost", 10); (* Entry 2 -- RG 91 entry 2 (§4): Sacred Triduum. Thu-Sat of Holy Week, NOT entry 7 (which stops at Wednesday -- see entry 7 below). *) - ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 2); - ("2 Good Friday", off (-2), cand "ef-good-friday", 2); - ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 2); + ("2 Holy Thursday", off (-3), cand "ef-holy-thursday", 20); + ("2 Good Friday", off (-2), cand "ef-good-friday", 20); + ("2 Holy Saturday", off (-1), cand "ef-holy-saturday", 20); (* Entry 3 -- RG 91 entry 3 (§4). *) - ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 3); - ("3 Ascension", off 39, cand "ef-ascension", 3); - ("3 Trinity", off 56, cand "ef-trinity", 3); - ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 3); - ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 3); - ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 3); + ("3 Epiphany", mk 2026 1 6, cand "ef-epiphany", 30); + ("3 Ascension", off 39, cand "ef-ascension", 30); + ("3 Trinity", off 56, cand "ef-trinity", 30); + ("3 Corpus Christi", off 60, cand "ef-corpus-christi", 30); + ("3 Sacred Heart", off 68, cand "ef-sacred-heart", 30); + ("3 Christ the King", T.christ_the_king 2026, cand "ef-christ-the-king", 30); (* Entry 4 -- RG 91 entry 4 (§4). Sanctoral-origin: neither feast is part of temporal_ef's movable cycle. *) ( "4 Immaculate Conception", mk 2026 12 8, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-immaculate-conception", - 4 ); - ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 4); + 40 ); + ("4 Assumption", mk 2026 8 15, cand ~origin:P.Sanctoral ~subject:Sub.Bvm ~layer:PE.universal_layer "ef-assumption", 40); (* Entry 5 -- RG 91 entry 5 (§4). *) - ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 5); - ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 5); + ("5 Nativity Vigil", mk 2026 12 24, cand "ef-nativity-vigil", 50); + ("5 Octave day (Circumcision)", mk 2026 1 1, cand "ef-circumcision", 50); (* Entry 6 -- RG 91 entry 6 (§4). *) - ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 6); - ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 6); - ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 6); - ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 6); - ("6 Low Sunday", off 7, cand "ef-low-sunday", 6); + ("6 Advent Sunday", T.advent_start 2026, cand "ef-advent-sunday-1", 60); + ("6 Lent Sunday", off (-42), cand "ef-lent-sunday-1", 60); + ("6 Passion Sunday (I Passiontide)", off (-14), cand "ef-passion-sunday", 60); + ("6 Palm Sunday (II Passiontide)", off (-7), cand "ef-palm-sunday", 60); + ("6 Low Sunday", off 7, cand "ef-low-sunday", 60); (* Entry 7 -- RG 91 entry 7 (§4): Ash Wednesday and Mon/Tue/Wed of Holy Week ONLY -- Thu-Sat are entry 2 above, not this entry. *) - ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 7); - ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 7); - ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 7); - ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 7); + ("7 Ash Wednesday", off (-46), cand "ef-ash-wednesday", 70); + ("7 Monday of Holy Week", off (-6), cand "ef-holy-monday", 70); + ("7 Tuesday of Holy Week", off (-5), cand "ef-holy-tuesday", 70); + ("7 Wednesday of Holy Week", off (-4), cand "ef-holy-wednesday", 70); (* Entry 8 -- RG 91 entry 8 (§4). 2 Nov 2026 is a Monday (verified independently below the table), so this row is the plain case. The register's own qualifying case -- "yields to an occurring Sunday" -- gets its own row and its own end-to-end test after this table (2 Nov 2025 is a real Sunday). *) - ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 8); + ("8 All Souls (non-Sunday)", mk 2026 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 80); (* Entry 8's qualifier: "(yields to an occurring Sunday)". 2 Nov 2025 is - a Sunday, so this must NOT be 8 -- it must lose to entry 15 (16 = - entry 15's own value + 1, the exact value precedence_ef.ml documents - and justifies at entry 8's branch). The end-to-end resolve-level - proof that the Sunday actually wins the day is + a Sunday, so this must NOT be 80 -- it must lose to entry 15 (151 = + entry 15's own value, 150, + 1, the exact value precedence_ef.ml + documents and justifies at entry 8's branch). The end-to-end + resolve-level proof that the Sunday actually wins the day is [test_all_souls_yields_to_sunday] below; this row pins the specific integer [band] returns. *) - ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 16); + ("8 All Souls (yields to a Sunday, 2 Nov 2025)", mk 2025 11 2, cand ~origin:P.Sanctoral ~layer:PE.universal_layer "ef-all-souls", 151); (* Entry 9 -- RG 91 entry 9 (§4). *) - ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 9); + ("9 Pentecost Vigil", off 48, cand "ef-pentecost-vigil", 90); (* Entry 10 -- RG 91 entry 10 (§4): both range boundaries, to guard the off-by-one an inclusive Easter-offset window invites. *) - ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 10); - ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 10); - ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 10); - ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 10); + ("10 Easter octave, day+1", off 1, cand "ef-easter-1-mon", 100); + ("10 Easter octave, day+6", off 6, cand "ef-easter-1-sat", 100); + ("10 Pentecost octave, day+50", off 50, cand "ef-pentecost-1-mon", 100); + ("10 Pentecost octave, day+55", off 55, cand "ef-pentecost-1-sat", 100); (* Entry 11 -- RG 91 entry 11 (§4). *) ( "11 Universal I-class feast", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-ss-peter-paul", - 11 ); + 110 ); (* Entry 12 -- RG 91 entry 12 (§4). The one non-base-layer case the brief asks for explicitly: same date/rank/subject as 11, only the layer differs, so this row isolates the layer test as the deciding factor. *) ( "12 Proper I-class feast (non-base layer)", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-patron", - 12 ); + 120 ); (* Entry 13 -- RG 91 entry 13 (§4). *) ( "13 Indult I-class feast", mk 2026 6 29, cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant") "ef-indult-feast-1", - 13 ); + 130 ); + (* fix round 1 (coordinator finding 1), NEW: a synthetic I-CLASS + candidate landing INSIDE Holy Family's own 7-13 January window -- + the exact counter-example that found the original [-14] sentinel's + inversion (Holy Family used to beat every I-class entry outright, + not merely the fixed Baptism it was built to beat). Must still + outrank the movable-14 entry below (110 < entry_14_movable_band), + proving the fix, not merely the absence of the old bug's OWN + witness (13 January). Not live against the shipped universal data + (register §6.2) -- this is exactly the kind of I-class PROPER/ + INDULT feast an overlay could add, entries 11-13's own [not + is_temporal] tests notwithstanding. *) + ( "11 SYNTHETIC: a universal I-class feast inside Holy Family's own window \ + (fix round 1, coordinator finding 1) must still outrank the movable half", + mk 2026 1 11, + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer + "ef-synthetic-class1-probe", + 110 ); (* Entry 14 -- RG 91 entry 14 (§4), deliberately UNQUALIFIED (contrast entry 16, which explicitly says "not of the Lord"). *) ( "14 Feast of the Lord, II class", mk 2026 7 1, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-precious-blood", - 14 ); + PE.entry_14_fixed_band ); (* Entry 14, non-base layer: unlike 11-13/16/19/20/23/24, entry 14 draws - no universal/proper/indult line at all, so this must STILL be 14, not - 19 -- the exact restriction review finding 2 flagged and this row - exists to keep from silently coming back. *) + no universal/proper/indult line at all, so this must STILL be + [PE.entry_14_fixed_band], not 19 -- the exact restriction review + finding 2 flagged and this row exists to keep from silently coming + back. *) ( "14 Feast of the Lord, II class (non-base layer)", mk 2026 7 2, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:"diocese-warsaw" "ef-local-feast-of-the-lord", - 14 ); + PE.entry_14_fixed_band ); (* Entry 14, MOVABLE half -- RG 91 entry 14's own text (§4, scan-verified): "Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- movable FIRST, then fixed. Holy Family (RG 17(b), temporal_ef.ml's own @@ -147,35 +172,38 @@ let cases = 18's own Ember row above uses), not a hand-typed slug, so a rename or a dropped [~subject:Lord] on temporal_ef.ml's own side fails this row too, not only test_temporal_ef.ml's. Must be a value STRICTLY - BETTER than the fixed half's own literal 14 immediately above (not - merely different) -- the two are structurally disjoint by [is_temporal] - alone so they can never literally collide on any one candidate, but - {!compare_by}'s own kernel-level tie-break (Slug.compare, deliberately - NOT rubric-authoritative -- the exact anti-pattern RG 113's own fix - already corrected once, this file's own header) is what would decide - between them if they returned the SAME band on the one date they can - coincide (13 January, when it is itself Holy Family's date) -- - "commemoration-of-the-baptism-of-the-lord" sorts before - "ef-time-after-epiphany-sunday-1" alphabetically, backwards from RG - 91's own stated order. See [PE.entry_14_movable_band]'s own comment - in precedence_ef.ml for why the value is negative rather than some - other distinct integer. *) + BETTER than the fixed half ([PE.entry_14_fixed_band] immediately + above, not merely different) -- the two are structurally disjoint by + [is_temporal] alone so they can never literally collide on any one + candidate, but {!compare_by}'s own kernel-level tie-break + (Slug.compare, deliberately NOT rubric-authoritative -- the exact + anti-pattern RG 113's own fix already corrected once, this file's + own header) is what would decide between them if they returned the + SAME band on the one date they can coincide (13 January, when it is + itself Holy Family's date) -- "commemoration-of-the-baptism-of-the- + lord" sorts before "ef-time-after-epiphany-sunday-1" alphabetically, + backwards from RG 91's own stated order. CORRECTED, fix round 1 + (coordinator finding 1): [PE.entry_14_movable_band] used to be a + negative sentinel, which beat every I-class entry too, not merely + entry 14's own fixed half -- see the SYNTHETIC I-class probe row + above, and [PE.entry_14_movable_band]'s own comment in + precedence_ef.ml, for the fix and the counter-example that found it. *) ( "14 Feast of the Lord, II class, MOVABLE half (Holy Family, RG 17(b))", T.holy_family_sunday 2026, of_temporal (T.holy_family_sunday 2026), PE.entry_14_movable_band ); (* Entry 15 -- RG 91 entry 15 (§4): an ordinary Sunday not named at entry 6 -- Septuagesima is II class (RG 11-12 names only Advent/Lent/ Passiontide/Easter/Low/Pentecost as I class). *) - ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 15); + ("15 II-class Sunday (Septuagesima)", off (-63), cand ~rank:V.Class2 "ef-septuagesima-sunday", 150); (* Entry 16 -- RG 91 entry 16 (§4). *) ( "16 Universal II-class feast, not of the Lord", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:PE.universal_layer "ef-some-saint", - 16 ); + 160 ); (* Entry 17 -- RG 91 entry 17 (§4): days WITHIN the Nativity octave (26-28 Dec are Stephen/John/Innocents -- sanctoral, not this entry; 1 Jan is entry 5's Octave DAY, not this entry either). *) - ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 17); - ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 17); + ("17 Nativity octave, 29 Dec", mk 2026 12 29, cand ~rank:V.Class2 "ef-nativity-octave-day-5", 170); + ("17 Nativity octave, 31 Dec", mk 2026 12 31, cand ~rank:V.Class2 "ef-nativity-octave-day-7", 170); (* Entry 18 -- RG 91 entry 18 (§4): Advent 17-23 Dec ferias AND the Ember days of Advent/Lent/September share this one entry. The second row is deliberately a Lent date (season Lent, NOT Advent) to prove the @@ -183,21 +211,21 @@ let cases = to fall in the Dec 17-23 window -- the exact trap the brief warns about, worked the other way round: this Ember day must NOT be mistaken for an ordinary entry-22 Lent feria either. *) - ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 18); + ("18 Advent 17-23 Dec feria", mk 2026 12 21, cand ~rank:V.Class2 "ef-advent-4-mon", 180); (* Sourced from Temporal_ef.temporal's own output (see [of_temporal]) rather than a hand-typed "ef-lent-ember-wed" -- closes review finding 3's coupling concern for the Ember prefixes specifically. *) - ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 18); + ("18 Lent Ember Wednesday (from Temporal_ef.temporal)", off (-39), of_temporal (off (-39)), 180); (* Entry 19 -- RG 91 entry 19 (§4). *) ( "19 Proper II-class feast", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:"diocese-warsaw" "ef-local-saint-2", - 19 ); + 190 ); (* Entry 20 -- RG 91 entry 20 (§4). *) ( "20 Indult II-class feast", mk 2026 1 20, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Saint ~layer:(PE.indult_prefix ^ "local-grant-2") "ef-indult-feast-2", - 20 ); + 200 ); (* Entry 21 -- RG 91 entry 21 (§4, RG 28-34). Two rows: the Ascension Vigil is the one II-class vigil temporal_ef already produces today (temporal-origin); the Assumption Vigil stands in for the @@ -207,22 +235,22 @@ let cases = (* Sourced from Temporal_ef.temporal's own output (see [of_temporal]) rather than a hand-typed "ef-ascension-vigil" -- closes review finding 3's coupling concern for [vigil_suffix]. *) - ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 21); + ("21 Ascension Vigil (from Temporal_ef.temporal)", off 38, of_temporal (off 38), 210); ( "21 Assumption Vigil (sanctoral-origin)", mk 2026 8 14, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "ef-assumption-vigil", - 21 ); + 210 ); (* Also review finding 3 / "worth doing": a UNIVERSAL-layer Class2 vigil whose subject is the Lord must still be 21, not 14 -- pins entry 14's [not is_vigil] guard even after finding 2 dropped its layer test. *) ( "21 Universal II-class vigil of the Lord", mk 2026 6 23, cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer "ef-precious-blood-vigil", - 21 ); + 210 ); (* Entry 22 -- RG 91 entry 22 (§4) (corrected: ends at Palm Sunday, not Passion Sunday). Both a Lent and a Passiontide feria, clear of Ash Wednesday, Holy Week and the Ember days. *) - ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 22); - ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 22); + ("22 Lent feria", off (-41), cand ~rank:V.Class3 "ef-lent-1-mon", 220); + ("22 Passiontide feria", off (-12), cand ~rank:V.Class3 "ef-passiontide-1-tue", 220); (* Entry 23 -- RG 91 entry 23 (§4). NOTE the table's own order here is the REVERSE of 11/12 and 14/16/19/20 above: entry 23 (particular calendars) is numbered BELOW entry 24 (universal), so a proper @@ -230,45 +258,45 @@ let cases = register states it, not "corrected" to match the other classes. *) ( "23 Proper III-class feast (non-base layer)", mk 2026 6 30, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-saint-3", - 23 ); + 230 ); (* Entry 24 -- RG 91 entry 24 (§4). *) ( "24 Universal III-class feast", mk 2026 6 30, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-some-saint-3", - 24 ); + 240 ); (* Entry 25 -- RG 91 entry 25 (§4). *) - ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 25); + ("25 Advent feria to 16 Dec", mk 2026 12 1, cand ~rank:V.Class3 "ef-advent-1-tue", 250); (* Entry 26 -- RG 91 entry 26 (§4). *) ( "26 III-class vigil", mk 2026 8 9, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "ef-lawrence-vigil", - 26 ); + 260 ); (* Also worth doing: a NON-universal-layer Class3 vigil must still be 26, not 23 -- pins entry 23's [not is_vigil] guard. *) ( "26 III-class vigil (non-base layer)", mk 2026 8 10, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:"diocese-warsaw" "ef-local-patron-vigil", - 26 ); + 260 ); (* Task 11, issue (a): the sanctoral bootstrap (data/ef/sanctoral.sexp) names its vigils with lectio's OWN "vigil-of-X" PREFIX convention, not [PE.vigil_suffix]'s "-vigil" SUFFIX every row above uses -- exactly the mismatch Task 7's review predicted. These two rows use the real bootstrapped slugs verbatim (data/ef/sanctoral.sexp: 28 Jun, 9 Aug), proving [band] recognises the prefix convention too: without it, both - would misfile at 16/24 (an ordinary feast of the same rank) instead - of 21/26. *) + would misfile at 160/240 (an ordinary feast of the same rank) instead + of 210/260. *) ( "21 II-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", mk 2026 6 28, cand ~origin:P.Sanctoral ~rank:V.Class2 ~layer:PE.universal_layer "vigil-of-sts-peter-paul", - 21 ); + 210 ); ( "26 III-class vigil via the sanctoral data's own \"vigil-of-X\" prefix", mk 2026 8 9, cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence", - 26 ); + 260 ); (* Entry 27 -- RG 91 entry 27 (§4): an otherwise-unoccupied IV-class Saturday. *) ( "27 Office of the BVM on Saturday", off 62, cand ~rank:V.Class4 "ef-time-after-pentecost-1-sat", - 27 ); + 270 ); (* Entry 28 -- RG 91 entry 28 (§4): the unqualified IV-class catch-all. *) - ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 28); + ("28 IV-class feria", off 65, cand ~rank:V.Class4 "ef-time-after-pentecost-1-tue", 280); (* Not an RG 91 row at all: a I-class candidate marked as a vigil, which is not the Nativity or Pentecost (entries 5/9, the only I-class vigils the table names) and so has no entry to fall into. Proves the @@ -1343,6 +1371,44 @@ let test_holy_family_excludes_baptism_end_to_end () = [ "commemoration-of-the-baptism-of-the-lord" ] (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted) +(* Fix round 1 (coordinator finding 1), end-to-end proof of the + [entry_14_movable_band] fix itself: a genuine I-class candidate landing + inside Holy Family's own 7-13 January window (a synthetic proper/indult + feast, standing in for the diocesan-overlay shape {!band}'s own new + comment names as the live risk) must be OBSERVED and must NOT transfer + -- reproducing, at the [Precedence.resolve] level, the exact regression + the coordinator demonstrated against the pre-fix [-14] sentinel ("the + I-class feast displaced and transferred to the next day"). With the fix, + {!band}'s entry 11 (110) beats {!PE.entry_14_movable_band} (139) + outright, so the I-class candidate wins the day, and Holy Family itself + -- losing, is_temporal, its own slug carrying the "-sunday" marker -- + falls to RG 109(a)'s ordinary privileged-Sunday-commemoration path + (disposition's final catch-all, [privilege_of]'s (a) branch), the same + treatment ANY genuine feast outranking an ordinary Sunday gets: neither + omitted nor transferred, a Privileged commemoration. *) +let test_class1_feast_inside_holy_familys_window_end_to_end () = + let date = mk 2026 1 11 in + Alcotest.(check string) "sanity: this is Holy Family's own date in 2026" "2026-01-11" (D.to_iso8601 date); + let day_ctx = ctx date in + let holy_family = of_temporal date in + let synthetic_class1 = + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe" + in + let resolution = P.resolve real_rules day_ctx ~temporal:holy_family ~sanctoral:[ synthetic_class1 ] in + Alcotest.(check string) "the I-class candidate is observed, NOT Holy Family (RG91 e11 beats e14-movable)" + "ef-synthetic-class1-probe" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check (list string)) "nothing transfers -- the pre-fix bug moved the I-class feast to the next day" + [] (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred); + (match resolution.P.commemorations with + | [ (c, P.Privileged) ] -> + Alcotest.(check string) + "Holy Family itself is privileged-commemorated (RG109(a), a Sunday-shaped office \ + outranked by a genuine feast), neither omitted nor transferred" + "ef-time-after-epiphany-sunday-1" (S.to_string c.P.cel.Cel.slug) + | other -> + Alcotest.failf "expected exactly one Privileged commemoration (Holy Family), got %d" + (List.length other)) + (* Completes Task 7's carried fix (RG 91 entry 8, §4): on a real Sunday landing on 2 November, All Souls does not merely lose (that was Task 7's [band] fix, proved by [test_all_souls_yields_to_sunday] above) -- it must @@ -1594,6 +1660,10 @@ let suite = "RG112(a) + RG91 e14 \"primum mobilia\": Holy Family excludes the Baptism of the Lord \ entirely, end-to-end (13 Jan 2019)" `Quick test_holy_family_excludes_baptism_end_to_end; + Alcotest.test_case + "fix round 1, finding 1: a synthetic I-class candidate inside Holy Family's own window is \ + observed and does not transfer (2026-01-11)" + `Quick test_class1_feast_inside_holy_familys_window_end_to_end; Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day" `Quick test_transfer_target_general_multi_step_search; Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday" -- cgit v1.3 From ac277cd1aaa47401b4a4c205b54c2faf88938c73 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 09:16:30 +0200 Subject: precedence(ef): fix round 1 (F2, F7) -- correct a false transcription claim, cite RG95's real primary authority F2: the disposition comment for RG112(a) claimed the electronic transcription "carries NEITHER this paragraph's own worked example NOR its own more specific Mass-proper rubric... exactly the transcription's documented defect" -- wrong on both counts, checked directly rather than taken on trust. RG 112 has no worked example in any of the three source documents (the transcription included) -- there was nothing to drop. The Holy Family Mass propers' own 13-January rubric is absent from the transcription not because it silently drops content it otherwise has (the project's own documented defect, dropped calendarium commemoration lines) but because that specific document (a 2006 web capture) contains almost no Mass-propers text of any day: 5 Introit-line occurrences across 26,322 lines, against 61 and 402 in the two photographic scans; its own "Proprium de Tempore" page is a table of contents linking to PDFs the capture never pulled in. A coverage gap in a partial web capture, not an instance of the narrower, different defect the methodology note actually describes. The ruling is unaffected -- the propers' quotation is real, on two independent photographic scans, and settles 13 January on its own -- only the attribution is struck, here and in test_precedence_ef.ml (the register and the task report are corrected separately). F7: RG 95's own second paragraph -- present in all three documents, including the transcription -- is a more direct, occurrence-level authority for the exact collision than RG 112(a): "if two feasts of the same Divine Person... occur together, the one higher in the table is kept, the other omitted." Added as the primary citation on disposition's RG112(a) branch, with RG 112(a) and the propers' note kept as corroboration. Also corrects RG 112(b)'s own framing: it is bidirectional ("et vicissim"), and only the Lord-feast-wins direction (RG 16(a)) is implemented -- the Sunday-wins direction has no witness anywhere in this codebase's data and was wrongly described as already covered. --- lib/rites/rite_ef/precedence_ef.ml | 86 +++++++++++++++++++++++++------------ lib/rites/rite_ef/precedence_ef.mli | 17 +++++--- test/test_precedence_ef.ml | 67 ++++++++++++++++++----------- 3 files changed, 111 insertions(+), 59 deletions(-) (limited to 'test') diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 39251a7..635a69f 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -822,20 +822,49 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) Precedence.Omit else if winner.Precedence.cel.Celebration.subject = Subject.Lord && cel.Celebration.subject = Subject.Lord then - (* RG 112(a) (docs/research/rules-register.md §4/§6.0, primary text, - verified against BOTH photographic scans -- missale-romanum-1962.pdf - and "Missale Romanum 1962_text.pdf" -- the electronic transcription, - 1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum,_LT.pdf, carries - NEITHER this paragraph's own worked example NOR its own more - specific Mass-proper rubric below, exactly the transcription's - documented defect, this project's own methodology note): "112. Ad - commemorationes et orationes quod attinet, haec insuper serventur: - a) Officium, Missa aut commemoratio de aliquo festo vel mysterio - UNIUS DIVINAE PERSONAE excludit commemorationem aut orationem de - alio festo vel mysterio EIUSDEM DIVINAE PERSONAE" -- the Office, - Mass, or commemoration of some feast or MYSTERY of ONE Divine Person - excludes a commemoration or oration of ANOTHER feast or mystery of - the SAME Divine Person. + (* PRIMARY AUTHORITY -- CORRECTED, fix round 1 (coordinator finding 7): + RG 95's own SECOND paragraph (Caput XIII, "De dierum liturgicorum + occurrentia accidentali", immediately after the "only I-class feasts + translate" sentence {!disposition}'s own Transfer branch above + already cites), present verbatim in ALL THREE documents including + the electronic transcription -- an OCCURRENCE-level rule, closer to + this exact question than RG 112(a) below: "Si vero duo festa eiusdem + Divinae Personae aut duo festa eiusdem Sancti vel Beati simul + occurrunt, fit de festo, quod in tabella praecedentiae superiorem + obtinet locum et aliud omittitur" -- but if two feasts of the SAME + DIVINE PERSON, or two feasts of the same Saint or Blessed, occur + TOGETHER, [the Office] is made of the feast which holds the HIGHER + PLACE in the table of precedence, and the OTHER IS OMITTED. This is + the direct authority: two feasts of the same Divine Person + (RG 112(a)'s own vocabulary) occurring together, the higher-table one + kept, the other omitted -- exactly {!band}'s own + [entry_14_movable_band]/[entry_14_fixed_band] ordering plus this + branch's own [Omit], not [Commemorate]. + + CORROBORATION 1, RG 112(a) (docs/research/rules-register.md §4/§6.0, + primary text, verified against all three documents -- CORRECTED, fix + round 1 (coordinator finding 2): a previous version of this comment + claimed the electronic transcription drops this paragraph's own + worked example and the Mass-proper rubric below, "the transcription's + documented defect" -- WRONG on both counts, struck. RG 112 has NO + worked example in any of the three documents; there was nothing to + drop. The Mass-proper rubric's absence from the transcription is not + an instance of that document's documented defect either (dropped + CALENDARIUM commemoration lines) -- checked directly: that specific + transcription (1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum, + _LT.pdf, a 2006 web capture) contains almost no Mass-propers text of + any kind (5 "Introitus"/"Antiphona ad Introitum" occurrences in + 26,322 lines, against 61 and 402 in the two photographic scans); its + own "Proprium de Tempore" page is a TABLE OF CONTENTS linking to + separate PDF files the capture never pulled in. A coverage gap in a + partial web capture, not a silent drop from content it otherwise + has): "112. Ad commemorationes et orationes quod attinet, haec + insuper serventur: a) Officium, Missa aut commemoratio de aliquo + festo vel mysterio UNIUS DIVINAE PERSONAE excludit commemorationem + aut orationem de alio festo vel mysterio EIUSDEM DIVINAE PERSONAE" -- + the Office, Mass, or commemoration of some feast or MYSTERY of ONE + Divine Person excludes a commemoration or oration of ANOTHER feast + or mystery of the SAME Divine Person. {!Subject.t} has no finer split within [Lord] than "a mystery touching the Divine Person of the Son" -- every real [subject = @@ -849,19 +878,20 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) by Person would need a finer [Subject.t] first, not a special case added here. - The one live witness -- confirmed DIRECTLY, not only by this general - paragraph: the Holy Family Mass propers' own note, immediately - following the Postcommunion, word for word on BOTH photographic - scans: "Si festum S. Familiae occurrerit die 13 ianuarii, Missa - dicitur de festo S. Familiae, SINE COMMEMORATIONE BAPTISMATIS - D.N.I.C., et sine commemoratione dominicae" -- if the feast of the - Holy Family occurs on 13 January, the Mass is said of the Holy - Family, WITHOUT commemoration of the Baptism of Our Lord Jesus - Christ, and without commemoration of the Sunday (RG 17(b)'s own - general rule, restated for this specific date). Independently - confirmed against missalemeum (register §6.0): title "The Holy - Family: Jesus, Mary & Joseph", commemorations [], the Baptism - listed only under "displaced", never commemorated. + CORROBORATION 2 -- the Holy Family Mass propers' own note, + immediately following the Postcommunion, word for word on BOTH + photographic scans (real, and settles 13 January on its own even + apart from RG 95/112(a) -- the quotation itself is not in dispute, + only its earlier mis-attribution above): "Si festum S. Familiae + occurrerit die 13 ianuarii, Missa dicitur de festo S. Familiae, SINE + COMMEMORATIONE BAPTISMATIS D.N.I.C., et sine commemoratione + dominicae" -- if the feast of the Holy Family occurs on 13 January, + the Mass is said of the Holy Family, WITHOUT commemoration of the + Baptism of Our Lord Jesus Christ, and without commemoration of the + Sunday (RG 17(b)'s own general rule, restated for this specific + date). Independently confirmed against missalemeum (register §6.0): + title "The Holy Family: Jesus, Mary & Joseph", commemorations [], + the Baptism listed only under "displaced", never commemorated. Checked after RG 16(a) above (so a genuine RG 16(a) Sunday- suppression is never re-explained under this citation instead) and @@ -869,7 +899,7 @@ let disposition ~(winner : Vocab_ef.rank Precedence.candidate) without it a [Feast]-status loser of this shape would otherwise reach RG 95's ordinary "commemorated or omitted" branch and be admitted as an ordinary Class2 commemoration under RG 111(b) -- - confirmed wrong against both primary sources above. *) + confirmed wrong against all three primary sources above. *) Precedence.Omit else (* RG 95's other branch: "aut commemorantur aut penitus omittuntur" -- diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 239a254..7d0bc98 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -159,13 +159,18 @@ val sunday_marker : string commemoration of a losing SAINT freely, with no such class-keyed gate); - a loser whose [subject] is [Lord] AND whose WINNER's [subject] is also - [Lord] is [Omit] (RG 112(a) -- a feast or mystery of one Divine Person - excludes a commemoration of another feast or mystery of the SAME - Divine Person; live witness: Holy Family, RG 17(b), excluding the - fixed Commemoration of the Baptism of the Lord on the years 13 + [Lord] is [Omit] (PRIMARY authority: RG 95's own second paragraph, + present in all three source documents including the electronic + transcription -- "if two feasts of the SAME DIVINE PERSON... occur + together, [the Office] is made of the one higher in the table of + precedence, and the OTHER IS OMITTED"; corroborated by RG 112(a) -- + a feast or mystery of one Divine Person excludes a commemoration of + another feast or mystery of the SAME Divine Person -- and by the Holy + Family Mass propers' own more specific 13-January rubric on both + photographic scans; live witness: Holy Family, RG 17(b), excluding + the fixed Commemoration of the Baptism of the Lord on the years 13 January is itself Holy Family's own date -- see the .ml's own comment - for the primary text, including the Holy Family Mass propers' own - more specific 13-January rubric, on both photographic scans); + for the full primary text of all three); - everything else -- including an impeded I-class Sunday, and a SANCTORAL loser of any rank below I class -- is [Commemorate], carrying its real RG 109 privilege (see {!admit} below); RG 109(a) diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 98ae0c8..4ee2fd8 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -715,31 +715,48 @@ let disposition_cases = "ef-synthetic-class3-lord-feast", an_ordinary_sunday, "Commemorate(Privileged)" ); - (* RG 112(a) (docs/research/rules-register.md §4/§6.0, primary text, - verified against BOTH photographic scans): "112. Ad commemorationes - et orationes quod attinet, haec insuper serventur: a) Officium, Missa - aut commemoratio de aliquo festo vel mysterio unius Divinae Personae - excludit commemorationem aut orationem de alio festo vel mysterio - eiusdem Divinae personae" -- the Office, Mass or commemoration of - some feast or MYSTERY of ONE Divine Person excludes a commemoration - or oration of ANOTHER feast or mystery of the SAME Divine Person. - Real witness, the ONE date these two candidates can ever coincide: - Holy Family (RG 17(b)) as winner, the Commemoration of the Baptism - of the Lord (13 January, data/ef/sanctoral.sexp: Class2, subject - Lord, status Feast) as loser, on the seven years 13 January is - itself Holy Family's own date (2008, 2013, 2019, 2030, 2036, 2041, - 2047). Directly confirmed by the Missal's own MORE SPECIFIC rubric, - found in the Holy Family Mass propers themselves (both photographic - scans, word for word): "Si festum S. Familiae occurrerit die 13 - ianuarii, Missa dicitur de festo S. Familiae, SINE COMMEMORATIONE - BAPTISMATIS D.N.I.C., et sine commemoratione dominicae" -- if the - feast of the Holy Family occurs on 13 January, the Mass is said of - the Holy Family, WITHOUT commemoration of the Baptism, and without - commemoration of the Sunday (RG 17(b)'s own general rule, restated). - The electronic transcription (1962-06-23,_SS_Ioannes_XXIII,_Missale - _Romanum,_LT.pdf) carries NEITHER sentence at all -- exactly the - transcription's own documented defect (this project's methodology - note), not merely absent from this one check. *) + (* PRIMARY authority -- CORRECTED, fix round 1 (coordinator finding 7): + RG 95's own second paragraph, present in ALL THREE documents + including the electronic transcription: "Si vero duo festa eiusdem + Divinae Personae... simul occurrunt, fit de festo, quod in tabella + praecedentiae superiorem obtinet locum et aliud omittitur" -- if two + feasts of the SAME DIVINE PERSON occur together, the one higher in + the table is kept, the OTHER OMITTED -- an occurrence-level rule + closer to this exact collision than RG 112(a) below. + CORROBORATION, RG 112(a) (docs/research/rules-register.md §4/§6.0, + primary text, verified against all three documents): "112. Ad + commemorationes et orationes quod attinet, haec insuper serventur: + a) Officium, Missa aut commemoratio de aliquo festo vel mysterio + unius Divinae Personae excludit commemorationem aut orationem de + alio festo vel mysterio eiusdem Divinae personae" -- the Office, + Mass or commemoration of some feast or MYSTERY of ONE Divine Person + excludes a commemoration or oration of ANOTHER feast or mystery of + the SAME Divine Person. Real witness, the ONE date these two + candidates can ever coincide: Holy Family (RG 17(b)) as winner, the + Commemoration of the Baptism of the Lord (13 January, data/ef/ + sanctoral.sexp: Class2, subject Lord, status Feast) as loser, on the + seven years 13 January is itself Holy Family's own date (2008, 2013, + 2019, 2030, 2036, 2041, 2047). + CORROBORATION 2, the Holy Family Mass propers' own more specific + rubric, both photographic scans, word for word (real, and settles 13 + January on its own regardless of RG 95/112(a)): "Si festum S. + Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S. + Familiae, SINE COMMEMORATIONE BAPTISMATIS D.N.I.C., et sine + commemoratione dominicae" -- if the feast of the Holy Family occurs + on 13 January, the Mass is said of the Holy Family, WITHOUT + commemoration of the Baptism, and without commemoration of the + Sunday (RG 17(b)'s own general rule, restated). + CORRECTED, fix round 1 (coordinator finding 2): a previous version of + this comment claimed the electronic transcription's absence of this + specific rubric was "exactly the transcription's own documented + defect" -- WRONG, struck. That transcription (a 2006 web capture) + carries almost no Mass-propers text of any kind (5 Introit + occurrences in 26,322 lines against 61/402 in the two photographic + scans; its own "Proprium de Tempore" page is a table of contents + linking to PDFs the capture never pulled in) -- a coverage gap, not + a silent drop from content it otherwise has. RG 112 itself, unlike + this rubric, IS present in full in the transcription (all four + sub-clauses, verbatim) -- checked directly, not assumed. *) ( "RG112(a): a feast/mystery of the Lord excludes a commemoration of \ ANOTHER feast/mystery of the Lord -- Omit, not Commemorate (Holy \ Family vs the Baptism of the Lord, 13 January)", -- cgit v1.3 From b7da41445d89cc83e749ec19c57e321ceb6d5cf2 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 09:16:39 +0200 Subject: temporal(ef): fix round 1 (F3) -- give Holy Family a primary-sourced Latin name On 7,197 of the 8,417 days Holy Family occurs (every year that isn't the 13-January collision), every emitted byte was identical to the pre-change binary -- subject appeared in no output path the CLI or any test read. Celebration.make already takes ?names, defaulting to empty, and Temporal_ef.build never passed it. build now takes an optional ?names, set only on Holy Family's own branch: a LATIN name, not English. The oracle's own observed-identity axis (test_oracle.ml) reads only `en`, so this is deliberately invisible to it -- an English name here would mean reading missalemeum's own title text, the oracle this exact axis is compared against, to decide colitur's "ground truth" name: the "expected value promoted from actual output" vacuity flavour this project's review process watches for. Latin has no such circularity: the calendarium's own January table and the Mass propers' own heading, both photographic scans, word for word, "Sanctae Familiae Iesu, Mariae, Ioseph" -- the same genitive-title convention test_names.ml's own worked example already uses for Easter ("Dominica Resurrectionis"). Every other temporal-cycle candidate, including the neighbouring Sundays, still carries no name of any kind -- a targeted addition for the one day this task built, not a claim that the general gap (register's own open item on Holy Name of Jesus, RG 17(a)) is closed. --- lib/rites/rite_ef/temporal_ef.ml | 41 +++++++++++++++++++++++++++++++++------- test/test_temporal_ef.ml | 16 ++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) (limited to 'test') diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml index 0647aac..1ce9de9 100644 --- a/lib/rites/rite_ef/temporal_ef.ml +++ b/lib/rites/rite_ef/temporal_ef.ml @@ -444,9 +444,10 @@ let temporal d = let easter = Computus.gregorian_easter y in let s = season d in let weekday = Date.weekday d in - let build ?(subject = Colitur_kernel.Subject.Temporal) ~season ~slug ~colour ~rank ~week () = + let build ?(subject = Colitur_kernel.Subject.Temporal) ?(names = Colitur_kernel.Names.empty) ~season + ~slug ~colour ~rank ~week () = let office = - Colitur_kernel.Celebration.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject + Colitur_kernel.Celebration.make ~slug:(Slug.of_string_exn slug) ~rank ~colour ~subject ~names ~layer:"temporal" () in { Colitur_kernel.Temporal.season; week; weekday; office } @@ -508,12 +509,38 @@ let temporal d = OTHER Sunday this branch ever builds -- including the narrower Holy Name of Jesus window, RG 17(a), still unbuilt as its own named day, register §6 -- keeps - [Temporal], unaffected. *) - let subject = - if same d (holy_family_sunday y) then Colitur_kernel.Subject.Lord - else Colitur_kernel.Subject.Temporal + [Temporal] and no name, unaffected. + + [names] -- fix round 1 (coordinator finding 3): a LATIN + name, not English. The oracle's own observed-identity + axis (test_oracle.ml) reads only [en], so this is + deliberately invisible to it -- setting an ENGLISH name + here would mean reading missalemeum's own title text + (the oracle this exact axis is compared against) to + decide colitur's own "ground truth" name, precisely the + "expected value promoted from actual output" vacuity + flavour this project's own review process watches for. + Latin has no such circularity: the calendarium's own + January table, both photographic scans, word for word: + "Dominica I post Epiphaniam: S. Familiae, Iesu, Mariae, + Ioseph, II classis" -- and the Mass propers' own heading + (also both scans): "SANCTAE FAMILIAE IESU, MARIAE, + IOSEPH, II classis", the exact string used here, the + same genitive-title convention test_names.ml's own + worked example already uses for Easter ("Dominica + Resurrectionis"). Every other temporal-cycle candidate + still carries no name at all (register §6.2's own open + 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. *) + 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 (Colitur_kernel.Subject.Temporal, Colitur_kernel.Names.empty) in - build ~subject ~season:s ~slug ~colour ~rank ~week:(week d) () + build ~subject ~names ~season:s ~slug ~colour ~rank ~week:(week d) () | None -> ( match christmastide_feria_slug d with | Some slug -> diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml index 7837fb5..8a0f38e 100644 --- a/test/test_temporal_ef.ml +++ b/test/test_temporal_ef.ml @@ -175,6 +175,9 @@ let rank_of dt = V.rank_to_string (office dt).Cel.rank let colour_of dt = Colr.to_string (office dt).Cel.colour let subject_of dt = (office dt).Cel.subject let temporal_week dt = (T.temporal dt).Colitur_kernel.Temporal.week +let la = Colitur_kernel.Lang.of_string_exn "la" +let en = Colitur_kernel.Lang.of_string_exn "en" +let name_la_of dt = Colitur_kernel.Names.find (office dt).Cel.names la (* RG 17(b) (Caput III, "De Dominicis"), primary text (scan-verified, docs/research/rules-register.md §4): "festum S. Familiae Iesu, Mariae, @@ -207,6 +210,19 @@ let test_holy_family () = classis\" -- the mystery of the Holy Family touches Christ)" true (subject_of (d 2026 1 11) = Sub.Lord); + (* fix round 1 (coordinator finding 3): a primary-sourced LATIN name, + zero circularity (the calendarium's own January table and the Mass + propers' own heading, both photographic scans, word for word: + "Sanctae Familiae Iesu, Mariae, Ioseph"). No ENGLISH name -- deliberate + (temporal_ef.ml's own comment), and every OTHER temporal-cycle + candidate still carries no name of any kind. *) + Alcotest.(check (option string)) "2026: Latin name from the calendarium/propers, verbatim" + (Some "Sanctae Familiae Iesu, Mariae, Ioseph") (name_la_of (d 2026 1 11)); + Alcotest.(check (option string)) "2026: no English name (deliberate -- would read the oracle it is \ + compared against)" + None (Colitur_kernel.Names.find (office (d 2026 1 11)).Cel.names en); + Alcotest.(check (option string)) "2026: an ordinary neighbouring Sunday carries no Latin name either" + None (name_la_of (d 2026 1 18)); (* 2019: Epiphany itself is a Sunday, so the first Sunday AFTER it is the LATEST possible date, 13 January -- the one date Holy Family collides with the fixed Commemoration of the Baptism of the Lord (data/ef/ -- cgit v1.3 From b9b26ef4badc2ef4dfff2e57decf7f9c57066888 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 09:19:51 +0200 Subject: test(golden): fix round 1 (F4) -- describe pins subject, the field RG16(a)/RG112(a) key on Two of the three original Holy Family pins (2026-01-11, ordinary year; 2024-01-07, earliest possible date) were byte-identical before and after temporal_ef.ml's own fix. Reverting the feature failed only the 2030-01-13 collision pin -- describe omitted subject, the ONE field the change actually touches on an ordinary (non-collision) year, so those two pins asserted an ordinary Sunday's identity and called it Holy Family. Vacuity flavour #1 (an assertion already true before the code under test ran), not #8 (promoted from actual output) -- the other nine fields' values were independently sourced and remain so. subject added to describe's own formatted line. Every existing golden pin's expected string updated to match -- each value independently sourced (grep data/ef/sanctoral.sexp for the sanctoral-origin pins; Temporal for every ef-* temporal slug) before running the suite to verify, not copied from actual output and rationalised afterward. All matched on the first attempt. --- test/test_golden.ml | 108 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 46 deletions(-) (limited to 'test') diff --git a/test/test_golden.ml b/test/test_golden.ml index 259b81a..1724375 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -93,9 +93,19 @@ let fetch y m d = Cal.day Rite_ef.context real_ef_layer (mk y m d) let slug_s (c : V.rank Cel.t) = Slug.to_string c.Cel.slug let rank_s (c : V.rank Cel.t) = V.rank_to_string c.Cel.rank let colour_s (c : V.rank Cel.t) = Colour.to_string c.Cel.colour +let subject_s (c : V.rank Cel.t) = Colitur_kernel.Subject.to_string c.Cel.subject let privilege_s = function Prec.Privileged -> "privileged" | Prec.Ordinary -> "ordinary" -(* The five fields {!Colitur_kernel.Liturgical_day.t} promises are never +(* fix round 1 (coordinator finding 4): [subject] added. Two of the three + original Holy Family pins (2026-01-11, 2024-01-07) were byte-identical + before and after temporal_ef.ml's own fix -- reverting the feature failed + only the 2030-01-13 collision pin, because [describe] omitted the ONE + field the change touches on an ordinary (non-collision) year. Vacuity + flavour #1 ("an assertion already true before the code under test ran"), + not #8 ("promoted from actual output") -- the OTHER nine fields' values + were independently sourced and remain so; only this tenth was missing. + + The five fields {!Colitur_kernel.Liturgical_day.t} promises are never silently lost, folded into one comparable line -- see the file header for why one string, not five checks. *) let describe (day : (V.season, V.rank) LD.t) = @@ -110,11 +120,12 @@ let describe (day : (V.season, V.rank) LD.t) = List.map (fun (c, d) -> Printf.sprintf "%s->%s" (slug_s c) (Date.to_iso8601 d)) day.LD.transferred_out |> List.sort compare |> String.concat "," in - Printf.sprintf "%s %s season=%s week=%s slug=%s rank=%s colour=%s comms=[%s] in=%s out=[%s]" + Printf.sprintf "%s %s season=%s week=%s slug=%s rank=%s colour=%s subject=%s comms=[%s] in=%s out=[%s]" (Date.to_iso8601 day.LD.date) (Date.weekday_to_string t.Temporal.weekday) (V.season_to_string t.Temporal.season) - week (slug_s day.LD.observed) (rank_s day.LD.observed) (colour_s day.LD.observed) comms tin tout + week (slug_s day.LD.observed) (rank_s day.LD.observed) (colour_s day.LD.observed) + (subject_s day.LD.observed) comms tin tout let check ~msg y m d expected = Alcotest.(check string) msg expected (describe (fetch y m d)) @@ -139,11 +150,11 @@ let omitted_has (day : (V.season, V.rank) LD.t) slug = place, not merely that 22 March is white. *) let test_easter_extreme_1598 () = check ~msg:"1598-03-21 Holy Saturday: still Passiontide, violet (RG 128)" 1598 3 21 - "1598-03-21 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet \ + "1598-03-21 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=temporal \ comms=[] in=- out=[]"; check ~msg:"1598-03-22 earliest possible Easter (Gauss-verified): Paschaltide begins, white, class-1" 1598 3 22 - "1598-03-22 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white comms=[] in=- \ + "1598-03-22 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal comms=[] in=- \ out=[]" (* 1666: latest Gregorian Easter possible, 25 April -- hand-verified via @@ -151,11 +162,11 @@ let test_easter_extreme_1598 () = 25 April). Same citations as 1598 above. *) let test_easter_extreme_1666 () = check ~msg:"1666-04-24 Holy Saturday: still Passiontide, violet (RG 128)" 1666 4 24 - "1666-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet \ + "1666-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=temporal \ comms=[] in=- out=[]"; check ~msg:"1666-04-25 latest possible Easter (Gauss-verified): Paschaltide begins, white, class-1" 1666 4 25 - "1666-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white comms=[] in=- \ + "1666-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal comms=[] in=- \ out=[]" (* 2038: the brief's "late modern case" -- Easter also 25 April (Gauss- @@ -180,10 +191,10 @@ let test_easter_extreme_1666 () = though nothing forces that any more). *) let test_easter_extreme_2038_late_modern () = check ~msg:"2038-04-24 Holy Saturday: still Passiontide, violet (RG 128)" 2038 4 24 - "2038-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet \ + "2038-04-24 saturday season=passiontide week=2 slug=ef-passiontide-2-saturday rank=class-1 colour=violet subject=temporal \ comms=[] in=- out=[]"; check ~msg:"2038-04-25 late-modern instance of the latest possible Easter (Gauss-verified)" 2038 4 25 - "2038-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white comms=[] in=- \ + "2038-04-25 sunday season=paschaltide week=1 slug=ef-easter-sunday rank=class-1 colour=white subject=temporal comms=[] in=- \ out=[]" (* ------------------------------------------------------------------ *) @@ -204,7 +215,7 @@ let test_annunciation_transfer_inside_holy_week_2016 () = actually reaches a live case" for exactly this reason. *) check ~msg:"2016-03-25 Good Friday: Triduum outranks the Annunciation (RG91 entry 2 > 11), no commemoration" 2016 3 25 - "2016-03-25 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet \ + "2016-03-25 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet subject=temporal \ comms=[] in=- out=[annunciation-of-the-blessed-virgin-mary->2016-04-04]"; (* The general RG 96 walk from 26 March would still be inside the Triduum, the Easter octave (all I class, entry 2/10) -- carrying the feast PAST @@ -212,7 +223,7 @@ let test_annunciation_transfer_inside_holy_week_2016 () = after Low Sunday = Easter + 8 = 27 Mar + 8 = 4 April. *) check ~msg:"2016-04-04 Easter+8 (Monday after Low Sunday): RG96 Attamen(a) sedes propria fires" 2016 4 4 "2016-04-04 monday season=paschaltide week=2 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \ - colour=white comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]" + colour=white subject=saint comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]" (* ------------------------------------------------------------------ *) (* 2008: the double transfer (register §4, "cases worth adding"). Easter = @@ -227,7 +238,7 @@ let test_annunciation_joseph_double_transfer_2008 () = I-class feria, band 7, above 11) -- impeded, Transfer-disposed. *) check ~msg:"2008-03-19 Wednesday of Holy Week outranks Joseph (RG91 entry 7 > 11), no commemoration" 2008 3 19 - "2008-03-19 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet \ + "2008-03-19 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet subject=temporal \ comms=[] in=- out=[joseph-spouse-of-the-bl-virgin-mary->2008-04-01]"; (* Both Joseph and the Annunciation are band 11 (register: neither is Immaculate Conception/Assumption, band 4; both are ordinary universal @@ -255,7 +266,7 @@ let test_annunciation_joseph_double_transfer_2008 () = check ~msg:"2008-03-31 Easter+8: Annunciation's named sedes propria (RG96 Attamen a) claims the day first" 2008 3 31 "2008-03-31 monday season=paschaltide week=2 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \ - colour=white comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]"; + colour=white subject=saint comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]"; (* Joseph's own general RG96 walk, finding 31 March already claimed (occupant reports Class1 there, still blocking per [is_blocking]), continues one more day to 1 April -- RG 97-98's "in order": the second @@ -264,7 +275,7 @@ let test_annunciation_joseph_double_transfer_2008 () = check ~msg:"2008-04-01: Joseph's RG96 walk continues past the now-claimed 31 March (RG97-98 'in order')" 2008 4 1 "2008-04-01 tuesday season=paschaltide week=2 slug=joseph-spouse-of-the-bl-virgin-mary rank=class-1 \ - colour=white comms=[] in=joseph-spouse-of-the-bl-virgin-mary out=[]" + colour=white subject=saint comms=[] in=joseph-spouse-of-the-bl-virgin-mary out=[]" (* ------------------------------------------------------------------ *) (* RG 96 Attamen (a) is CONDITIONAL ("quando est transferendum post @@ -288,15 +299,15 @@ let test_annunciation_exception_not_triggered_general_walk_suffices () = check ~msg:"2057-03-26: general RG96 walk suffices (target before Easter, 22 Apr) -- no Attamen(a) exception" 2057 3 26 "2057-03-26 monday season=lent week=3 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \ - colour=white comms=[ef-lent-3-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]"; + colour=white subject=saint comms=[ef-lent-3-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]"; check ~msg:"2007-03-26: general RG96 walk suffices (target before Easter, 8 Apr) -- no Attamen(a) exception" 2007 3 26 "2007-03-26 monday season=passiontide week=1 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \ - colour=white comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]"; + colour=white subject=saint comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]"; check ~msg:"2012-03-26: general RG96 walk suffices (target before Easter, 8 Apr) -- no Attamen(a) exception" 2012 3 26 "2012-03-26 monday season=passiontide week=1 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \ - colour=white comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]" + colour=white subject=saint comms=[ef-passiontide-1-monday:privileged] in=annunciation-of-the-blessed-virgin-mary out=[]" (* After Easter: 2016 (already pinned above, Easter+8 = 4 April) and 2024 (Easter 31 March, Easter+8 = 8 April). In both, the general walk would @@ -312,7 +323,7 @@ let test_annunciation_exception_triggered_when_walk_would_cross_easter () = (RG26)" 2024 4 8 "2024-04-08 monday season=paschaltide week=2 slug=annunciation-of-the-blessed-virgin-mary rank=class-1 \ - colour=white comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]" + colour=white subject=saint comms=[] in=annunciation-of-the-blessed-virgin-mary out=[]" (* 2016's own pin (test_annunciation_transfer_inside_holy_week_2016) is the other half of this pair -- not repeated here to avoid asserting the same date twice. *) @@ -330,19 +341,19 @@ let test_annunciation_exception_triggered_when_walk_would_cross_easter () = let test_precious_blood_transfer_2011 () = check ~msg:"2011-07-01: Sacred Heart (RG91 entry 3) outranks the fixed Precious Blood (entry 11) outright" 2011 7 1 - "2011-07-01 friday season=time-after-pentecost week=2 slug=ef-sacred-heart rank=class-1 colour=white \ + "2011-07-01 friday season=time-after-pentecost week=2 slug=ef-sacred-heart rank=class-1 colour=white subject=temporal \ comms=[] in=- out=[precious-blood-of-our-lord-jesus-christ->2011-07-04]"; check ~msg:"2011-07-02: the RG96 walk's first blocked day (Visitation, II class) -- Precious Blood not here" 2011 7 2 "2011-07-02 saturday season=time-after-pentecost week=2 slug=visitation-of-the-blessed-virgin-mary \ - rank=class-2 colour=white comms=[processus-and-martinian:ordinary] in=- out=[]"; + rank=class-2 colour=white subject=saint comms=[processus-and-martinian:ordinary] in=- out=[]"; check ~msg:"2011-07-03: the RG96 walk's second blocked day (an ordinary Sunday, II class) -- still not here" 2011 7 3 "2011-07-03 sunday season=time-after-pentecost week=3 slug=ef-time-after-pentecost-sunday-3 rank=class-2 \ - colour=green comms=[] in=- out=[]"; + colour=green subject=temporal comms=[] in=- out=[]"; check ~msg:"2011-07-04: the first non-I/II-class day (RG96) -- the Precious Blood lands here" 2011 7 4 "2011-07-04 monday season=time-after-pentecost week=3 slug=precious-blood-of-our-lord-jesus-christ \ - rank=class-1 colour=red comms=[] in=precious-blood-of-our-lord-jesus-christ out=[]" + rank=class-1 colour=red subject=lord comms=[] in=precious-blood-of-our-lord-jesus-christ out=[]" (* ------------------------------------------------------------------ *) (* All Souls falling on a Sunday (register §4, RG96 Attamen (b), primary- @@ -357,11 +368,11 @@ let test_all_souls_on_a_sunday_2025 () = check ~msg:"2025-11-02: All Souls yields to the occurring Sunday (RG91 entry 8), no commemoration (RG94)" 2025 11 2 "2025-11-02 sunday season=time-after-pentecost week=21 slug=ef-time-after-pentecost-sunday-21 rank=class-2 \ - colour=green comms=[] in=- out=[commemoration-of-all-souls->2025-11-03]"; + colour=green subject=temporal comms=[] in=- out=[commemoration-of-all-souls->2025-11-03]"; check ~msg:"2025-11-03: All Souls transfers to the following Monday, its sedes propria (RG96 Attamen b)" 2025 11 3 "2025-11-03 monday season=time-after-pentecost week=21 slug=commemoration-of-all-souls rank=class-1 \ - colour=black comms=[] in=commemoration-of-all-souls out=[]" + colour=black subject=saint comms=[] in=commemoration-of-all-souls out=[]" (* ------------------------------------------------------------------ *) (* Christmas falling on a Sunday (RG91 entry 1: Nativity, Easter, Pentecost @@ -375,7 +386,7 @@ let test_all_souls_on_a_sunday_2025 () = let test_christmas_on_a_sunday_2022 () = check ~msg:"2022-12-25 falling on a Sunday: the Nativity (RG91 entry 1) takes the day outright, no contest" 2022 12 25 - "2022-12-25 sunday season=christmastide week=- slug=ef-nativity rank=class-1 colour=white comms=[] in=- \ + "2022-12-25 sunday season=christmastide week=- slug=ef-nativity rank=class-1 colour=white subject=temporal comms=[] in=- \ out=[]" (* ------------------------------------------------------------------ *) @@ -388,18 +399,18 @@ let test_christmas_on_a_sunday_2022 () = confirmed via `date -d`. *) let test_holy_thursday_is_white_2026 () = check ~msg:"2026-04-01 (Wednesday of Holy Week): ordinary Passiontide violet (RG128)" 2026 4 1 - "2026-04-01 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet \ + "2026-04-01 wednesday season=passiontide week=2 slug=ef-passiontide-2-wednesday rank=class-1 colour=violet subject=temporal \ comms=[] in=- out=[]"; check ~msg: "2026-04-02 Holy Thursday: white (RG128(b)'s named exception + RG122's affirmative statement), not \ Passiontide's violet" 2026 4 2 - "2026-04-02 thursday season=passiontide week=2 slug=ef-passiontide-2-thursday rank=class-1 colour=white \ + "2026-04-02 thursday season=passiontide week=2 slug=ef-passiontide-2-thursday rank=class-1 colour=white subject=temporal \ comms=[] in=- out=[]"; check ~msg:"2026-04-03 Good Friday: back to violet (RG128) -- Thursday's white is a one-day exception" 2026 4 3 - "2026-04-03 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet \ + "2026-04-03 friday season=passiontide week=2 slug=ef-passiontide-2-friday rank=class-1 colour=violet subject=temporal \ comms=[] in=- out=[]" (* ------------------------------------------------------------------ *) @@ -419,14 +430,14 @@ let test_ember_ferias_commemorated_when_impeded () = makes it PRIVILEGED, not merely eligible. *) check ~msg:"1900-12-21: Advent Ember Friday, impeded, MUST be commemorated (RG24) and is privileged (RG109e)" 1900 12 21 - "1900-12-21 friday season=advent week=3 slug=thomas rank=class-2 colour=red \ + "1900-12-21 friday season=advent week=3 slug=thomas rank=class-2 colour=red subject=saint \ comms=[ef-advent-ember-fri:privileged] in=- out=[]"; (* 1902-02-22: Lent Ember Saturday (RG91 entry 18, II class) impeded by the Chair of St Peter (II class, universal, entry 16). Same RG24/ RG109(e) mandate. *) check ~msg:"1902-02-22: Lent Ember Saturday, impeded, MUST be commemorated (RG24) and is privileged (RG109e)" 1902 2 22 - "1902-02-22 saturday season=lent week=1 slug=chair-of-st-peter rank=class-2 colour=white \ + "1902-02-22 saturday season=lent week=1 slug=chair-of-st-peter rank=class-2 colour=white subject=saint \ comms=[ef-lent-ember-sat:privileged] in=- out=[]" let test_iv_class_ferias_never_commemorated () = @@ -443,7 +454,7 @@ let test_iv_class_ferias_never_commemorated () = true (omitted_has d1 "ef-time-after-pentecost-8-wednesday"); Alcotest.(check string) "2026-07-22: Mary Magdalene observed, no commemoration (RG26)" - "2026-07-22 wednesday season=time-after-pentecost week=8 slug=mary-magdalene rank=class-3 colour=white \ + "2026-07-22 wednesday season=time-after-pentecost week=8 slug=mary-magdalene rank=class-3 colour=white subject=saint \ comms=[] in=- out=[]" (describe d1); let d2 = fetch 2026 8 10 in @@ -451,7 +462,7 @@ let test_iv_class_ferias_never_commemorated () = true (omitted_has d2 "ef-time-after-pentecost-11-monday"); Alcotest.(check string) "2026-08-10: St Lawrence observed, no commemoration (RG26)" - "2026-08-10 monday season=time-after-pentecost week=11 slug=lawrence rank=class-2 colour=red comms=[] in=- \ + "2026-08-10 monday season=time-after-pentecost week=11 slug=lawrence rank=class-2 colour=red subject=saint comms=[] in=- \ out=[]" (describe d2) @@ -476,7 +487,7 @@ let test_ii_class_sunday_admits_only_ii_class_commemoration () = (omitted_has d2009 "romanus"); Alcotest.(check string) "2009-08-09: the Sunday observed, no commemoration at all (RG111(b) rank floor)" "2009-08-09 sunday season=time-after-pentecost week=10 slug=ef-time-after-pentecost-sunday-10 rank=class-2 \ - colour=green comms=[] in=- out=[]" + colour=green subject=temporal comms=[] in=- out=[]" (describe d2009); let d2026 = fetch 2026 8 9 in Alcotest.(check bool) "2026-08-09: romanus (Class3) is offered and excluded, proving RG111(b) actually fired" @@ -484,7 +495,7 @@ let test_ii_class_sunday_admits_only_ii_class_commemoration () = (omitted_has d2026 "romanus"); Alcotest.(check string) "2026-08-09: the Sunday observed, no commemoration at all (RG111(b) rank floor)" "2026-08-09 sunday season=time-after-pentecost week=11 slug=ef-time-after-pentecost-sunday-11 rank=class-2 \ - colour=green comms=[] in=- out=[]" + colour=green subject=temporal comms=[] in=- out=[]" (describe d2026) (* ------------------------------------------------------------------ *) @@ -549,7 +560,7 @@ let test_rg16a_lord_feast_no_sunday_commemoration_2028 () = et privilegiis\" keeps the day a dominica II classis for RG111(b), so Sixtus (Class3) has no standing \ either, oracle-confirmed" "2028-08-06 sunday season=time-after-pentecost week=9 slug=transfiguration-of-our-lord rank=class-2 \ - colour=white comms=[] in=- out=[]" + colour=white subject=lord comms=[] in=- out=[]" (describe d) (* Holy Family (RG 17(b), scan-verified, docs/research/rules-register.md @@ -597,26 +608,31 @@ let test_rg16a_lord_feast_no_sunday_commemoration_2028 () = Sunday of it this is), slug=ef-time-after-epiphany-sunday-1 (unchanged from the plain Sunday key this date always carried -- temporal_ef.ml's own comment on why RG 17(b) does not need a new lectionary key), rank - =class-2, colour=white. *) + =class-2, colour=white -- AND, fix round 1 (coordinator finding 4), + subject=lord: the ONE field that actually distinguishes Holy Family from + an ordinary, unnamed Sunday (every other field above was already + identical before temporal_ef.ml's own fix -- [describe] originally + omitted [subject], so two of these three pins would have stayed green + even with the feature fully reverted; see [describe]'s own comment). *) let test_holy_family_ordinary_year_2026 () = check ~msg:"2026-01-11: Holy Family, ordinary year -- Hyginus (Class3, Commemoration_only) has no \ standing for the day's II-class-only Sunday slot" 2026 1 11 "2026-01-11 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \ - colour=white comms=[] in=- out=[]" + colour=white subject=lord comms=[] in=- out=[]" let test_holy_family_excludes_baptism_2030 () = check ~msg:"2030-01-13: Holy Family on the latest possible date -- RG91 e14 \"primum mobilia\" keeps it \ observed over the fixed Baptism, RG112(a) excludes the Baptism entirely, not merely demotes it" 2030 1 13 "2030-01-13 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \ - colour=white comms=[] in=- out=[]" + colour=white subject=lord comms=[] in=- out=[]" let test_holy_family_earliest_possible_date_2024 () = check ~msg:"2024-01-07: Holy Family on the earliest possible date (Epiphany a Saturday)" 2024 1 7 "2024-01-07 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \ - colour=white comms=[] in=- out=[]" + colour=white subject=lord comms=[] 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 @@ -649,7 +665,7 @@ let test_purification_on_a_sunday_2025 () = commemoration of the Sunday, oracle-confirmed" 2025 2 2 "2025-02-02 sunday season=time-after-epiphany week=4 slug=purification-of-the-blessed-virgin-mary \ - rank=class-2 colour=white comms=[] in=- out=[]" + rank=class-2 colour=white subject=lord comms=[] in=- out=[]" (* ------------------------------------------------------------------ *) (* RG 91 entry 24 ("Festa III classis, in calendario Ecclesiae universae @@ -708,15 +724,15 @@ let test_iii_class_feast_wins_class4_feria () = true (omitted_has d1 "ef-easter-2-wednesday"); Alcotest.(check string) "2008-04-02: Francis of Paola (III class, RG91 entry 24) outranks the class-4 feria" - "2008-04-02 wednesday season=paschaltide week=2 slug=francis-of-paola rank=class-3 colour=white comms=[] \ + "2008-04-02 wednesday season=paschaltide week=2 slug=francis-of-paola rank=class-3 colour=white subject=saint comms=[] \ in=- out=[]" (describe d1); check ~msg:"2008-04-04: Isidore of Seville (III class) outranks the class-4 Paschaltide feria, same shape" 2008 4 4 - "2008-04-04 friday season=paschaltide week=2 slug=isidore-of-seville rank=class-3 colour=white comms=[] in=- \ + "2008-04-04 friday season=paschaltide week=2 slug=isidore-of-seville rank=class-3 colour=white subject=saint comms=[] in=- \ out=[]"; check ~msg:"2008-04-05: Vincent Ferrer (III class) outranks the class-4 Paschaltide feria, same shape" 2008 4 5 - "2008-04-05 saturday season=paschaltide week=2 slug=vincent-ferrer rank=class-3 colour=white comms=[] in=- \ + "2008-04-05 saturday season=paschaltide week=2 slug=vincent-ferrer rank=class-3 colour=white subject=saint comms=[] in=- \ out=[]"; let d4 = fetch 2038 3 6 in Alcotest.(check bool) @@ -726,15 +742,15 @@ let test_iii_class_feast_wins_class4_feria () = Alcotest.(check string) "2038-03-06: Sts Perpetua and Felicity (III class, RG91 entry 24) outrank the class-4 feria; RED per their \ own martyrs' colour, not the season's violet" - "2038-03-06 saturday season=septuagesima week=2 slug=sts-felicitas-perpetua rank=class-3 colour=red comms=[] \ + "2038-03-06 saturday season=septuagesima week=2 slug=sts-felicitas-perpetua rank=class-3 colour=red subject=saint comms=[] \ in=- out=[]" (describe d4); check ~msg:"2038-03-08: John of God (III class) outranks the class-4 Septuagesima feria, same shape" 2038 3 8 - "2038-03-08 monday season=septuagesima week=3 slug=john-of-god rank=class-3 colour=white comms=[] in=- \ + "2038-03-08 monday season=septuagesima week=3 slug=john-of-god rank=class-3 colour=white subject=saint comms=[] in=- \ out=[]"; check ~msg:"2038-03-09: Frances of Rome (III class) outranks the class-4 Septuagesima feria, same shape" 2038 3 9 - "2038-03-09 tuesday season=septuagesima week=3 slug=frances-rome rank=class-3 colour=white comms=[] in=- \ + "2038-03-09 tuesday season=septuagesima week=3 slug=frances-rome rank=class-3 colour=white subject=saint comms=[] in=- \ out=[]" let suite = -- cgit v1.3 From 9e6f099f7f1dd918cf5f9f418ed5e2408289dd40 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 09:20:33 +0200 Subject: test(temporal_ef): fix round 1 (F5) -- the anchor erosion test proved nothing test_holy_family_anchor_present_and_erosion_is_caught built its "eroded" list by filtering the exact pair it then asserted was absent -- removing an element and checking it is gone proves List.filter works, not that any detector caught anything. Borrowed the name of test_anchors_erosion_is_caught, which uses a genuine one: missing_from_anchors, called with an "expected" set sourced independently of T.anchors (named_slugs_for_year, via T.named). Reworked to match that shape: the expected slug is now sourced from T.sunday_slug (season/weekday logic, a different function entirely from T.anchors's own hand-typed string), and both the presence and erosion checks reuse the real missing_from_anchors detector instead of a bespoke, self-referential one. --- test/test_temporal_ef.ml | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) (limited to 'test') diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml index 8a0f38e..23a817d 100644 --- a/test/test_temporal_ef.ml +++ b/test/test_temporal_ef.ml @@ -542,21 +542,44 @@ let test_anchors_erosion_is_caught () = anchor check (validate.ml, register/spec §5.7) calls [temporal], not [named], so it is unaffected by this distinction and already covers this entry regardless; this is this file's own unit-level guard against the - same erosion. *) + same erosion. + + CORRECTED, fix round 1 (coordinator finding 5): the original version of + this test built [eroded] by [List.filter]-ing the exact pair it then + asserted [List.mem ... eroded = false] against -- tautological (removing + an element from a list and then checking it is not there proves nothing + about a DETECTOR; it proves [List.filter] works). The genuine detector + the OTHER erosion test above actually exercises is [missing_from_anchors] + itself, called with an "expected" set sourced INDEPENDENTLY of + [T.anchors] ([named_slugs_for_year], via [T.named] -- a different + function entirely) -- the interesting claim is that the SAME slug is + confirmed by two independent computations, not merely that a filtered + list lacks what was filtered out of it. Reworked to match: the + "expected" side here is [T.sunday_slug expected_date] -- computed via + season/weekday logic, not read out of [T.anchors]'s own hand-typed + string -- so the presence check below is a genuine cross-check between + two independent sources, and the erosion check reuses the real + [missing_from_anchors] detector rather than re-deriving a bespoke, + self-referential one. *) let test_holy_family_anchor_present_and_erosion_is_caught () = let y = 2026 in - let expected_slug = "ef-time-after-epiphany-sunday-1" in let expected_date = T.holy_family_sunday y in - Alcotest.(check bool) "the anchor entry exists, at the right date" true - (List.mem (expected_slug, expected_date) (T.anchors y)); - (* Erosion: with the entry struck out, [T.temporal] itself still puts - [expected_slug] at [expected_date] (unaffected -- deleting an [anchors] - row never touches [temporal]'s own computation, only what is CHECKED - against it), so a reader who only trusted [anchors] would no longer be - told to look there at all. *) - let eroded = List.filter (fun (s, dt) -> not (String.equal s expected_slug && dt = expected_date)) (T.anchors y) in - Alcotest.(check bool) "the erosion is caught: the entry no longer appears" false - (List.mem (expected_slug, expected_date) eroded); + let expected_slug = + match T.sunday_slug expected_date with + | Some s -> s + | None -> Alcotest.fail "sanity: holy_family_sunday must itself be a Sunday" + in + Alcotest.(check (list string)) + "the anchor entry exists: T.sunday_slug's own independent computation is not missing from T.anchors" [] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:(anchor_slugs y)); + (* Erosion: with the entry struck out of [anchors], [missing_from_anchors] + -- the SAME real detector, not a bespoke re-check -- must now report it + missing, using [expected_slug]'s own independent source ([sunday_slug]) + as the thing being checked FOR, not derived from the erosion itself. *) + let eroded_anchors = List.filter (fun s -> s <> expected_slug) (anchor_slugs y) in + Alcotest.(check (list string)) "the erosion is caught: missing_from_anchors reports it, and only it" + [ expected_slug ] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:eroded_anchors); Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion" expected_slug (slug_of expected_date) -- cgit v1.3 From 9df26e2e91646925c5977d4bab22c0d510d3b3c1 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 09:22:36 +0200 Subject: test(oracle): fix round 1 (F8) -- correct the M18 headline, name the blind spot precisely The full breakdown, precisely: of 730 days, 331 are resolved (330 matching, 1 mismatched -- M13, Joseph vs the Seven Sorrows), 399 unresolved -- 373 in M18's own bucket, 26 absorbed inside M1/M2/M3/ M16's own widened subsets where the axis fires alongside their pre-existing citation. M18's own note previously stated only its own 373 without this fuller context. Also names the blind spot the axis exists to close in exact terms, not just "the observed day's identity": a TEMPORAL-origin observed day silently replaced by a DIFFERENT temporal-origin observed day of the SAME rank and colour -- exactly Holy Family's own shape, and the reason every prior layer stayed green through the whole gap. States explicitly what the count pin does and does not prove: it guards against the 373/399 population moving silently in either direction, but cannot say which specific day moved or why. --- CLAUDE.md | 8 ++++- data/ef/expected-divergences-missalemeum.sexp | 2 +- test/test_oracle.ml | 52 ++++++++++++++++----------- 3 files changed, 40 insertions(+), 22 deletions(-) (limited to 'test') diff --git a/CLAUDE.md b/CLAUDE.md index 20f3ebf..8915bc6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,7 +124,13 @@ against missalemeum; layer 5 pins ~30 dates. fix below: resolvable only for a SANCTORAL-origin observed day; a TEMPORAL-origin one (an ordinary Sunday, a feria, a movable named feast — 373 of 730 days in the 2026–2027 window) is `Observed_identity_unresolved`, - counted and allow-listed (`M18`), never silently skipped. Teeth proved and + counted and allow-listed (`M18`), never silently skipped. Full breakdown: + 331 of 730 days resolved (330 matching, 1 mismatched — Joseph vs the Seven + Sorrows, `M13`), 399 unresolved (373 in `M18`, 26 absorbed inside four + other entries' own widened subsets). The blind spot, precisely: a + TEMPORAL-origin observed day silently replaced by a DIFFERENT + temporal-origin observed day of the SAME rank and colour — exactly Holy + Family's own shape. Teeth proved and reverted: corrupting one sanctoral saint's own English name on an OBSERVED day (rank/colour untouched) reddened the suite immediately with an `observed-identity-mismatch` and nothing else — the exact shape the diff --git a/data/ef/expected-divergences-missalemeum.sexp b/data/ef/expected-divergences-missalemeum.sexp index d7a1c8c..15f9cf2 100644 --- a/data/ef/expected-divergences-missalemeum.sexp +++ b/data/ef/expected-divergences-missalemeum.sexp @@ -165,5 +165,5 @@ ((id M18) (citation "test_oracle.ml's own header, \"COMMEMORATION IDENTITY\" section, restated for the OBSERVED axis (ef-rg112-rg110 task) -- not an RG citation, a comparator LIMIT: Rite_ef.Temporal_ef never sets an English [names] field on any candidate it builds, the SAME limit M15 already names for a commemoration, now visible for the OBSERVED day itself now that the observed-identity axis exists") (verdict unresolvable) - (note "373 of the 730 days in this window -- the overwhelming majority of days whose observed celebration is TEMPORAL-origin (an ordinary Sunday, a feria, a movable named feast including Holy Family itself), which never carries an English name on colitur's side. Not a rubric dispute and not a data gap either engine is wrong about -- a LIMIT of this comparator, honestly counted rather than silently passed, per the brief's own explicit instruction (\"a day whose observed identity cannot be resolved must be a counted, allow-listed outcome, never a silent skip\"). Gated on the diff SHAPE alone (exactly, and only, observed-identity-unresolved), not a literal date list the way every other entry in this file is -- at this population size a date list would itself be the \"pattern that could silently widen\" this file's own header warns against, for the opposite reason a date RANGE is normally risky: the predicate (colitur's own resolved name is [None]) is the precise, falsifiable evidence, the same shape M2's own title-substring predicate already uses instead of enumerating dates, just keyed on presence-of-a-name rather than a title string. Building an English name onto every temporal-cycle candidate (a data/lectionary-bootstrap task, Plan 4, out of this task's scope -- register §6, the same open item M15's own note already tracks) would resolve this entry to Matched for every one of these days it does not instead expose as a REAL divergence (Holy Family's own two dates in this window, 2026-01-11 and 2027-01-10, are counted here, not separately -- the name comparison genuinely cannot distinguish \"Holy Family\" from any other unnamed Sunday, so proving Holy Family's own correctness rests on the golden pins and the differential's own C15, not this axis). Derived directly from the comparator's own failure output (test_layer_m_counts_match_citations), not hand-counted first and cross-checked after: 373.") + (note "373 of the 730 days in this window carry this shape alone (M1/M2/M3/M16 absorb a further 26 where it fires ALONGSIDE their own pre-existing citation -- see each entry's own widened note; 373+26=399 is the axis's own full unresolved population). CORRECTED, fix round 1 (coordinator finding 8) -- the full breakdown, precisely: of 730 days, 331 are RESOLVED (colitur's own observed celebration carries a name) -- 330 resolved-and-MATCHING, 1 resolved-and-MISMATCHED (M13, Joseph vs the Seven Sorrows, already its own entry above) -- and 399 are UNRESOLVED (colitur's own name is [None]), split between this entry's own 373 and the 26 absorbed elsewhere. The BLIND SPOT this axis exists to close, stated precisely: a TEMPORAL-origin observed day silently replaced by a DIFFERENT temporal-origin observed day of the SAME RANK AND COLOUR -- exactly the shape that hid Holy Family from every layer before this task (rank 2/white on both sides, purely coincidental). This count PIN is what stands guard against that population growing silently in either direction: an implementation change that made MORE days temporal-origin-and-unresolvable, or fewer, changes 373 and fails the pin, even though the axis itself cannot say WHICH specific day moved or why. The overwhelming majority of days whose observed celebration is TEMPORAL-origin (an ordinary Sunday, a feria, a movable named feast including Holy Family itself) never carry an English name on colitur's side -- not a rubric dispute and not a data gap either engine is wrong about, a LIMIT of this comparator, honestly counted rather than silently passed, per the brief's own explicit instruction (\"a day whose observed identity cannot be resolved must be a counted, allow-listed outcome, never a silent skip\"). Gated on the diff SHAPE alone (exactly, and only, observed-identity-unresolved), not a literal date list the way every other entry in this file is -- at this population size a date list would itself be the \"pattern that could silently widen\" this file's own header warns against, for the opposite reason a date RANGE is normally risky: the predicate (colitur's own resolved name is [None]) is the precise, falsifiable evidence, the same shape M2's own title-substring predicate already uses instead of enumerating dates, just keyed on presence-of-a-name rather than a title string. Building an English name onto every temporal-cycle candidate (a data/lectionary-bootstrap task, Plan 4, out of this task's scope -- register §6, the same open item M15's own note already tracks) would resolve this entry to Matched for every one of these days it does not instead expose as a REAL divergence (Holy Family's own two dates in this window, 2026-01-11 and 2027-01-10, are counted here, not separately -- the name comparison genuinely cannot distinguish \"Holy Family\" from any other unnamed Sunday, so proving Holy Family's own correctness rests on the golden pins and the differential's own C15, not this axis). Derived directly from the comparator's own failure output (test_layer_m_counts_match_citations), not hand-counted first and cross-checked after: 373.") (expected_rows 373)) diff --git a/test/test_oracle.ml b/test/test_oracle.ml index 7068af9..35d4501 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -733,26 +733,38 @@ let m16_dates = [ "2026-03-27" ] TEMPORAL-origin one -- an ordinary Sunday, a feria, a movable named feast including Holy Family itself -- never does ({!Rite_ef.Temporal_ef}'s own [build] has no [en "..."] string anywhere in it, this file's own header - again). That is the OVERWHELMING majority of days in any calendar (saints' - feasts are common, but far from every day), so this is a single - STRUCTURAL predicate -- [Observed_identity_unresolved] alone, nothing - else disagreeing -- not a literal date list the way every other entry in - this file is: at 374 of the 730 days in this window, a list would be - exactly the "range/pattern that could silently widen" this file's own - header explicitly avoids elsewhere, for the OPPOSITE reason a range is - normally risky here -- the predicate itself (colitur's own name is - [None]) is the precise, falsifiable evidence, the same shape [M2]'s own - title-substring predicate already uses instead of a date list, just keyed - on presence-of-a-name rather than a title string. NOT a rubric dispute - and NOT a data gap either engine is wrong about -- a LIMIT of this - comparator itself, honestly counted rather than silently passed, per the - brief's own explicit instruction ("a day whose observed identity cannot - be resolved must be a counted, allow-listed outcome, never a silent - skip"), the SAME discipline [M15] already established for an unresolvable - COMMEMORATION, applied here for the first time to the OBSERVED day. Fixing - this at the root (giving every temporal-cycle candidate an English name) - is a data/lectionary-bootstrap task (Plan 4), not something this - comparator can do for itself -- register §6 tracks it, the same open item + again). CORRECTED, fix round 1 (coordinator finding 8) -- the precise + breakdown: of this window's 730 days, 331 are RESOLVED (330 matching, 1 + mismatched, [M13]); 399 are UNRESOLVED, split between this entry's own + 373 (the axis's sole disagreement) and 26 absorbed inside [M1]/[M2]/[M3]/ + [M16]'s own widened subsets, where it fires ALONGSIDE their pre-existing + citation. The BLIND SPOT this axis exists to close, stated precisely: a + TEMPORAL-origin observed day silently replaced by a DIFFERENT + temporal-origin observed day of the SAME RANK AND COLOUR -- exactly the + shape that hid Holy Family from every layer before this task. This is + the OVERWHELMING majority of days in any calendar (saints' feasts are + common, but far from every day), so this is a single STRUCTURAL + predicate -- [Observed_identity_unresolved] alone, nothing else + disagreeing -- not a literal date list the way every other entry in this + file is: at this population size, a list would be exactly the + "range/pattern that could silently widen" this file's own header + explicitly avoids elsewhere, for the OPPOSITE reason a range is normally + risky here -- the predicate itself (colitur's own name is [None]) is the + precise, falsifiable evidence, the same shape [M2]'s own title-substring + predicate already uses instead of a date list, just keyed on + presence-of-a-name rather than a title string. NOT a rubric dispute and + NOT a data gap either engine is wrong about -- a LIMIT of this comparator + itself, honestly counted rather than silently passed, per the brief's own + explicit instruction ("a day whose observed identity cannot be resolved + must be a counted, allow-listed outcome, never a silent skip"), the SAME + discipline [M15] already established for an unresolvable COMMEMORATION, + applied here for the first time to the OBSERVED day. The count pin below + ([test_layer_m_counts_match_citations]) is what stands guard against this + population growing (or shrinking) silently -- it cannot say WHICH day + moved or why, only that the total did. Fixing this at the root (giving + every temporal-cycle candidate an English name) is a data/lectionary- + bootstrap task (Plan 4), not something this comparator can do for itself + -- register §6 tracks it, the same open item [M15]'s own note already points to. *) let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = if diffs = [] then None -- cgit v1.3