diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/test_differential.ml | 53 | ||||
| -rw-r--r-- | test/test_golden.ml | 105 | ||||
| -rw-r--r-- | test/test_oracle.ml | 67 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 178 | ||||
| -rw-r--r-- | test/test_temporal_ef.ml | 148 |
5 files changed, 508 insertions, 43 deletions
diff --git a/test/test_differential.ml b/test/test_differential.ml index bea1bf4..5093484 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -138,6 +138,7 @@ module Slug = Colitur_kernel.Slug module Date = Colitur_kernel.Date module Cel = Colitur_kernel.Celebration module Colour = Colitur_kernel.Colour +module Subject = Colitur_kernel.Subject module V = Rite_ef.Vocab_ef (* Same relative paths test_rite_ef.ml/test_sanctoral_ef.ml use: dune test @@ -216,6 +217,14 @@ type row = { slug : string; rank : string; colour : string; + (* Fix round 1 (coordinator finding F3): ONLY ever populated for colitur's + own row (see [colitur_rows_2005_2050] below) -- lectio's own fixture + format has no subject column at all, so [row_of_line] (the lectio-side + parser) sets this to "-", a placeholder never read on that side. Every + Layer C predicate that reads [subject] must therefore read it off the + COLITUR row [c], never [l] -- the same asymmetry [row]'s other fields + do not have, called out here rather than left implicit. *) + subject : string; } let read_lines path = @@ -232,7 +241,7 @@ let read_lines path = let row_of_line line = match String.split_on_char ' ' line with | date :: weekday :: season :: week :: slug :: rank :: colour :: _others -> - { date; weekday; season; week; slug; rank; colour } + { date; weekday; season; week; slug; rank; colour; subject = "-" } | _ -> Alcotest.failf "malformed fixture line (fewer than 7 fields): %S" line let lectio_rows () = List.map row_of_line (read_lines fixture_path) @@ -271,7 +280,8 @@ let colitur_rows_2005_2050 () = week; slug = Slug.to_string cel.Cel.slug; rank = V.rank_to_string cel.Cel.rank; - colour = Colour.to_string cel.Cel.colour + colour = Colour.to_string cel.Cel.colour; + subject = Subject.to_string cel.Cel.subject } :: !rows | None -> Alcotest.failf "internal error: no resolved day for %s" (Date.to_iso8601 !d)); @@ -516,6 +526,44 @@ let holy_name_fallback_dates_2005_2050 = "2034-01-02"; "2035-01-02"; "2036-01-02"; "2040-01-02"; "2041-01-02"; "2045-01-02"; "2046-01-02"; "2047-01-02" ] +(* C17 -- ef-bvm-saturday task: RG 91 entry 27 ("Officium sanctae Mariae in + sabbato" -- see data/ef/expected-divergences.sexp's own C17 note for the + full RG 78/RG 120(b) citation). lectio builds no equivalent office at all + (checked directly against its own tridentine-calendar.ini/generator, the + same "genuine upstream gap, not a colitur bootstrap miss" shape C15/C16 + already document for their own gaps) -- it shows the ordinary season + colour on every otherwise-unoccupied Class4 Saturday where colitur's own + RG 78 fix now shows white. Season, slug and rank all still agree -- + temporal_ef.ml's own [bvm_saturday_names] citation ("Slug" paragraph) + deliberately REUSES the ordinary ferial slug rather than minting a new + one, precisely so this predicate's diff set stays [Colour_f] alone, never + [Slug_f] too. Gated on colitur's own [rank]/[weekday]/[colour] rather + than a date list, the same shape M2 (data/ef/expected-divergences- + missalemeum.sexp) already uses for the identical reason: this + population is large (a large fraction of all Saturdays domain-wide) and + entirely formulaic (RG 78's own condition, "otherwise unoccupied + IV-class Saturday", reduces exactly to this triple), not a short, + individually-interesting list of dates the way C14/C15/C16 above are. *) +let is_bvm_saturday_row (c : row) diffs = + (* Fix round 1 (coordinator finding F3): [c.subject] added -- the ONLY + field in this predicate that pins WHICH celebration is observed, not + merely its shape. Every other Layer C entry pins a colitur slug (C1's + own [jan_6_13_slug], C6's [nativity_octave_day_slugs], C8/C14/C15/C16's + own literal slug checks); this entry could not, because the office + deliberately REUSES the ordinary ferial slug (Rite_ef.Temporal_ef's own + [bvm_saturday_names] citation, "Slug" paragraph) -- there is no fixed + slug string to pin. [subject = "bvm"] is the field that DOES uniquely + identify the office (set nowhere else the differential's own + [colitur_rows_2005_2050] can produce a Saturday/Class4/white + combination for), closing the gap a shape-only predicate left open: + without this conjunct, a FUTURE bug that made some OTHER white, + Class4, Saturday candidate exist (Christmastide/Paschaltide, where + [season_colour] is already white, so a real bug there could slip + through unnoticed by colour alone) would be silently absorbed here + too. *) + diffs = [ Colour_f ] && String.equal c.weekday "saturday" && String.equal c.rank "class-4" + && String.equal c.colour "white" && String.equal c.subject "bvm" + (* [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). *) @@ -571,6 +619,7 @@ let layer_c_reason (l : row) (c : row) diffs = && subset diffs [ Slug_f; Rank ] && String.equal c.slug "ef-holy-name" then Some "C16" + else if is_bvm_saturday_row c diffs then Some "C17" else None (* ---------------------------------------------------------------------- *) diff --git a/test/test_golden.ml b/test/test_golden.ml index 594ea6b..fdfc01d 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -851,8 +851,22 @@ let test_iii_class_feast_wins_class4_feria () = "2008-04-05 saturday season=paschaltide week=2 slug=vincent-ferrer rank=class-3 colour=white subject=saint name_la=- comms=[] in=- \ out=[]"; let d4 = fetch 2038 3 6 in + (* ef-bvm-saturday task: 6 March 2038 is a SATURDAY, so the displaced + temporal candidate is now RG 78's own BVM Saturday office + ({!Rite_ef.Temporal_ef}'s own [bvm_saturday_names] citation) rather than + a bare ferial -- but per that citation's own "Slug" paragraph the SLUG + is deliberately left unchanged (reused, not replaced), precisely so + this test's own [omitted_has] probe still needs no update: the displaced + candidate is still named "ef-septuagesima-2-saturday", only its + colour/subject/name would have differed had it won. RG 91 entry 24 + (Sts Perpetua and Felicity) still outranks it either way -- entry 24 < + entry 27, both above entry 28 -- so the disposition/win outcome this + test proves is genuinely unchanged by this task, now more precisely + entry 24 > entry 27 rather than > 28 for this one date (a Saturday), not + that the test itself needed touching. *) Alcotest.(check bool) - "2038-03-06: the displaced class-4 Septuagesima feria is in [omitted] (RG26), proving RG91 entry24>28 fired" + "2038-03-06: the displaced class-4 feria (RG78's own BVM-Saturday office, \ + for this Saturday) is in [omitted] (RG26), proving RG91 entry24>27 fired" true (omitted_has d4 "ef-septuagesima-2-saturday"); Alcotest.(check string) @@ -869,6 +883,81 @@ let test_iii_class_feast_wins_class4_feria () = "2038-03-09 tuesday season=septuagesima week=3 slug=frances-rome rank=class-3 colour=white subject=saint name_la=- comms=[] in=- \ out=[]" +(* ------------------------------------------------------------------ *) +(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") -- + ef-bvm-saturday task, Rite_ef.Temporal_ef's own [bvm_saturday_names] + citation has the full primary-source argument. Two real-data pins, the + task brief's own worked example and its mirror-image negative case, + matching this file's own header rule: EVERY literal below was checked by + hand against `colitur day 2026` before being typed in here (this task's + own report has the full session log), never copied from a run and + rationalised after. *) + +(* The task brief's own worked example, checked against the oracle + (missalemeum) directly: "2026-08-01|4|w|V Mass of the B. V. M. -- Salve, + Sancta Parens|Saturday_after_IX_Sunday_after_Pentecost|Holy Machabees". + colitur's own rank (4) and colour (white) now match; the oracle's own + numbered-Mass title (RG 309(a)'s own "iuxta temporum diversitatem", not + modelled here -- {!bvm_saturday_names}'s own "Slug" paragraph) and the + observed day's own English identity remain outside what this comparator + can see (M18's own limit) -- this pin instead proves what colitur DOES + compute: white/class-4/subject Bvm/the Latin title, and Holy Machabees + (real data/ef/sanctoral.sexp entry, Commemoration_only, Class3, 1 August) + still admitted as an ORDINARY commemoration alongside it, matching the + oracle's own commemoration list exactly. *) +let test_bvm_saturday_2026 () = + check ~msg:"2026-08-01: the BVM Saturday office observed, white overriding Time after Pentecost's green, \ + Holy Machabees commemorated -- matches the task brief's own oracle example" + 2026 8 1 + "2026-08-01 saturday season=time-after-pentecost week=9 slug=ef-time-after-pentecost-9-saturday \ + rank=class-4 colour=white subject=bvm name_la=Officium sanctae Mariae in sabbato \ + comms=[holy-machabees:ordinary] in=- out=[]" + +(* The mirror-image case: RG 78's own protasis is FALSE here (a real feast + wins the Saturday outright), so the BVM-Saturday-shaped temporal + candidate must lose and be omitted under RG 26, exactly like the plain + ferial candidate it replaces always was -- no commemoration at all, + proving [band]/[disposition] need no special case for this office (the + design claim the task report makes explicitly). ALSO the one live data + witness where the winning candidate itself carries [subject = Bvm] + (data/ef/adjustments.sexp's own `Edit most-holy-name-of-mary + ((Set_subject Bvm))`) -- proving no Bvm-vs-Bvm confusion with a + RG112(a)-style subject rule: [omitted_has] below confirms the loser is + genuinely reported omitted (RG26 fires on [rank = Class4] alone, checked + ahead of any subject-based branch in {!Rite_ef.Precedence_ef.disposition}), + not silently swallowed by a same-subject exclusion that does not exist + for [Bvm] anywhere in this codebase. *) +let test_bvm_saturday_loses_to_most_holy_name_of_mary_2026 () = + let d = fetch 2026 9 12 in + Alcotest.(check bool) + "2026-09-12: the displaced BVM-Saturday-shaped candidate is in [omitted] (RG26), not silently absent" + true + (omitted_has d "ef-time-after-pentecost-15-saturday"); + Alcotest.(check string) + "2026-09-12: Most Holy Name of Mary (III class, subject Bvm, data/ef/adjustments.sexp) outranks the \ + class-4 BVM-Saturday candidate; no commemoration at all" + "2026-09-12 saturday season=time-after-pentecost week=15 slug=most-holy-name-of-mary rank=class-3 \ + colour=white subject=bvm name_la=- comms=[] in=- out=[]" + (describe d) + +(* RG112(d), fix round 1 (coordinator finding F1): PE.marian_slugs's own + citation (precedence_ef.ml) has the full RG112(d) argument, including + the 16 July "Missa dici potest aut ... aut ..." rubric found on both + photographic scans. 16 July 2033 is a real otherwise-unoccupied + Saturday; before this fix, [comms] here read + "[our-lady-of-mt-carmel:ordinary]" (confirmed against real pre-fix + `colitur day 2033` output). *) +let test_bvm_saturday_excludes_mt_carmel_2033 () = + let d = fetch 2033 7 16 in + Alcotest.(check bool) + "2033-07-16: Mt Carmel is in [omitted] (RG112(d)), not silently dropped from the record" true + (omitted_has d "our-lady-of-mt-carmel"); + Alcotest.(check string) + "2033-07-16: the BVM Saturday office observed, white; NO commemoration of Mt Carmel (RG112(d))" + "2033-07-16 saturday season=time-after-pentecost week=5 slug=ef-time-after-pentecost-5-saturday \ + rank=class-4 colour=white subject=bvm name_la=Officium sanctae Mariae in sabbato comms=[] in=- out=[]" + (describe d) + let suite = ( "golden pins (known-tricky years)", [ Alcotest.test_case "Easter extreme: 1598 earliest (22 Mar, Gauss-verified)" `Quick @@ -925,5 +1014,17 @@ let suite = 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)" - `Quick test_iii_class_feast_wins_class4_feria + `Quick test_iii_class_feast_wins_class4_feria; + Alcotest.test_case + "RG91 e27/RG78: the BVM Saturday office wins, Holy Machabees commemorated -- matches the task \ + brief's own oracle example (2026-08-01)" + `Quick test_bvm_saturday_2026; + Alcotest.test_case + "RG91 e27/RG78/RG26: the BVM Saturday office loses to Most Holy Name of Mary (subject Bvm too) \ + and is omitted, no confusion (2026-09-12)" + `Quick test_bvm_saturday_loses_to_most_holy_name_of_mary_2026; + Alcotest.test_case + "RG112(d), fix round 1 (F1): the BVM Saturday office excludes Mt Carmel's own commemoration \ + (2033-07-16)" + `Quick test_bvm_saturday_excludes_mt_carmel_2033 ] ) diff --git a/test/test_oracle.ml b/test/test_oracle.ml index 58ceb26..c0badfe 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -475,25 +475,30 @@ let contains_substring s ~needle = own "in dominica quavis" (ANY Sunday, unqualified) -- verdict colitur. *) let m1_dates = [ "2026-06-28"; "2026-08-09" ] -(* M2 -- RG 91 entry 27 ("Officium sanctae Mariae in sabbato"): every - otherwise-unoccupied IV-class Saturday should carry the votive Office of - the BVM (white; missalemeum's own title cycles "I".."V Mass of the - B. V. M. - Salve, Sancta Parens"), a genuinely DISTINCT office from an - ordinary green/violet feria. temporal_ef.ml's [band] already carries an - entry-27 comment acknowledging this row of RG 91 exists, but nothing in - [Temporal_ef.temporal]'s actual CONSTRUCTION builds this office -- an - unimpeded Time-after-Epiphany/-Pentecost Saturday still gets the bare - ferial slug and [season_colour]'s green. A genuine, primary-attested - FEATURE GAP (not a citation dispute) -- verdict missalemeum: colitur is - short a whole office here, not merely differently-opinioned. Registered - as an open item (register §6) rather than built in this task: it needs - its own proper texts/citations, the same scope line "Major Litanies" - (M5 below) already sits on. Matched on the oracle's own title substring, - not a hardcoded date list -- the title IS the distinguishing evidence, - and every Paschaltide occurrence (where colitur's season colour is - already white, RG 119) never reaches this predicate at all, since its - diffs are already empty before [layer_m_reason] is ever called. *) -let bvm_saturday_title = "Salve, Sancta Parens" +(* M2 -- CLOSED, REMOVED (ef-bvm-saturday task), same shape as M12/M17's own + removal elsewhere in this suite (not re-adjudicated to a different + verdict, gone because the underlying divergence stopped occurring): RG 91 + entry 27 ("Officium sanctae Mariae in sabbato") is now built + (Rite_ef.Temporal_ef's own [bvm_saturday_names] citation has the full RG + 78/RG 120(b) argument) -- white, unconditionally, on every otherwise- + unoccupied IV-class Saturday, so [Colour_f] no longer appears in this + predicate's own diff set on ANY of these 22 dates (checked: 0 of them + retain it). What is left on every one of them is [Observed_identity_ + unresolved] ALONE -- colitur's own BVM-Saturday candidate is + temporal-origin and deliberately carries no ENGLISH name (Latin only, + the same zero-circularity discipline Holy Family/Holy Name/the Sacred + Triduum already established) -- which is EXACTLY [M18]'s own shape + ([diffs = [ Observed_identity_unresolved ]], nothing else disagreeing), + not a distinct citation any more. Kept as a separate id here would have + been the same "count proving cardinality where identity was required" + trap this project's own review process watches for: [subset] admits a + SMALLER diff set than the one named, so this branch would have kept + silently absorbing these 22 rows under a stale "colour differs" citation + even though that citation's own reason no longer fires -- checked + directly, not assumed: removing this branch and re-running moves the + count from M2's own former 22 to M18's own count rising by exactly 22 + (373 -> 395, see M18's own note below), with the "no unexplained + differences" test staying green throughout. *) (* M3 -- RG 87 (Minor Litanies/Rogations): the SAME gap the lectio differential's own C8 already names (data/ef/expected-divergences.sexp) @@ -822,7 +827,12 @@ let m19_commemoration_matches (c : colitur_row) = | [ (slug, _, _, _) ] -> String.equal slug "commemoration-of-st-peter" | _ -> false -let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = +(* [_o] (the oracle's own row) is unused now that M2 -- its one reader, via + [o.o_title] -- is closed/removed (see the comment above [m1_dates]): + every remaining predicate below reads only [c]/[diffs]. Kept as a named, + typed parameter (not dropped from the signature) so a FUTURE oracle-title + predicate has somewhere to plug back in without a signature change. *) +let layer_m_reason (c : colitur_row) (_o : oracle_row) diffs = if diffs = [] then None (* M1's own subset widened (this task): colitur's observed day on both dates is TEMPORAL-origin (the Sunday itself, RG33's own point -- the @@ -832,17 +842,12 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = 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" + (* M2 -- CLOSED, REMOVED (ef-bvm-saturday task): see this file's own M2 + note above (near [m1_dates]) for the full account -- the RG 91 entry 27 + office is now built, so every one of these 22 dates now falls through + to [M18]'s own generic predicate below instead ([diffs = [ + Observed_identity_unresolved ]] exactly), which is what actually + catches them now. *) (* 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 diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index ed9a933..6f8b5b5 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -291,10 +291,21 @@ let cases = cand ~origin:P.Sanctoral ~rank:V.Class3 ~layer:PE.universal_layer "vigil-of-st-lawrence", 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", - 270 ); + Saturday. ef-bvm-saturday task: sourced from [Temporal_ef.temporal]'s + own real output (see [of_temporal], the same review-finding-3 + discipline entry 18's/21's own rows above use), not a hand-typed + slug/rank pair -- this row used to be the one place in this whole + suite where [band]'s entry-27 branch was exercised by a value nothing + in [Temporal_ef.temporal] could actually produce (the office itself + was unbuilt): [band] read [rank = Class4 && weekday = Sat] + correctly, but nothing proved [temporal] ever handed it a candidate + shaped that way for the RIGHT reason. Now it does: [of_temporal (off + 62)] is 6 June 2026, a real otherwise-contested Time-after-Pentecost + Saturday (Norbert, Class3, wins the DAY outright in the real + pipeline -- irrelevant here, since this row tests [band] on the + TEMPORAL CANDIDATE alone, which [Temporal_ef.temporal] builds as the + BVM Saturday office regardless of what wins). *) + ( "27 Office of the BVM on Saturday (from Temporal_ef.temporal)", off 62, of_temporal (off 62), 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", 280); (* Not an RG 91 row at all: a I-class candidate marked as a vigil, which @@ -794,6 +805,52 @@ let disposition_cases = "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)" ); + (* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's + own citation has the full argument (both scans, including the 16 + July "Missa dici potest aut ... aut ..." rubric). [of_temporal] on a + REAL 16-July-Saturday (2033) sources the winner from + Temporal_ef.temporal's own real output, the same review-finding-3 + discipline this file's Ember/vigil rows already use -- not a + hand-typed slug/subject pair that could silently drift from what the + office actually builds. The loser is the real data/ef/sanctoral.sexp + entry, [Commemoration_only], the exact shape that made this branch + need to be checked BEFORE the Commemoration_only catch-all. *) + ( "RG112(d): the BVM Saturday Office excludes a commemoration invoking \ + the SAME BVM's intercession -- Omit, not Commemorate (Mt Carmel, \ + 16 July 2033, a real otherwise-unoccupied Saturday)", + of_temporal (mk 2033 7 16), + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "our-lady-of-mt-carmel", + "Omit" ); + (* Conjunct 1/2, LOSER identity: an ORDINARY Commemoration_only saint + NOT on PE.marian_slugs must still reach the ordinary commemorate + flow even when the winner is the BVM Saturday Office -- proves + RG112(d) excludes a specific same-BVM collision, not every + commemoration the office happens to outrank (the real live shape, + 1 August, Holy Machabees, PE.marian_slugs's own comment). *) + ( "RG112(d) does NOT fire when the LOSER is not a Marian commemoration \ + -- an ordinary saint stays Commemorate(Ordinary) even under the BVM \ + Saturday Office (1 August 2026, Holy Machabees)", + of_temporal (mk 2026 8 1), + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "holy-machabees", + "Commemorate(Ordinary)" ); + (* Conjunct 2/2, WINNER identity: an ordinary (non-Bvm, non-Marian- + titled) winner must not exclude Mt Carmel either -- RG112(d) is + keyed on the WINNER also being "de B. Maria Virg.", not merely on + the loser's own identity. SYNTHETIC: no live date has an ordinary + Class3 saint winning outright while Mt Carmel also loses that same + day (Mt Carmel's own fixed date, 16 July, has no other real + sanctoral entry to construct this from) -- proves the conjunct is + real, not merely untested, the same "SYNTHETIC" discipline the + RG112(a) rows above already use for their own second conjunct. *) + ( "SYNTHETIC: RG112(d) does NOT fire when only the LOSER is Marian -- \ + an ordinary winner leaves Mt Carmel Commemorate(Ordinary)", + cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Saint ~layer:PE.universal_layer + "ef-synthetic-ordinary-winner", + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "our-lady-of-mt-carmel", "Commemorate(Ordinary)" ) ] @@ -1421,6 +1478,107 @@ let test_ii_class_sunday_privileged_witness_admitted_end_to_end () = [ ("ef-some-saint", "omitted: admission limit reached") ] (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) +(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") reached + end-to-end through the REAL pipeline -- this is THE hazard this task's own + brief names explicitly: {!PE.band}'s entry-27 branch has been reachable + only in isolation (the table-driven row above) since it was written, never + through a real [Temporal_ef.temporal] candidate winning an actual day, so + this test is checked for whether it exercises entry 27 for the RIGHT + reason -- proved by the mutation test recorded in the task report + (reverting temporal_ef.ml's own [is_bvm_saturday] branches back to the + generic ferial fallback reddens THIS test, not merely the [band]-table + row, which would still pass unchanged since it is sourced from real + [T.temporal] output either way). + + 1 August 2026, matching the task brief's own worked example against the + oracle exactly: a Time-after-Pentecost Saturday, otherwise unoccupied but + for Holy Machabees (Commemoration_only, Class3, real data/ef/sanctoral.sexp + entry, 1 August) -- the office wins outright (band 270, ahead of Holy + Machabees's own [unclassified]) and the Commemoration_only loser still + rides along as an ordinary commemoration, the SAME shape the oracle's own + row for this date shows ("V Mass of the B. V. M. -- Salve, Sancta Parens", + commemorating Holy Machabees). *) +let test_bvm_saturday_wins_and_commemorates_end_to_end () = + let date = mk 2026 8 1 in + let day_ctx = ctx date in + let bvm_saturday = of_temporal date in + let holy_machabees = + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~layer:PE.universal_layer + "holy-machabees" + in + let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ holy_machabees ] in + Alcotest.(check string) "the BVM Saturday office is observed, not Holy Machabees" + "ef-time-after-pentecost-9-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check string) "observed rank is class-4, unconditional" "class-4" + (V.rank_to_string resolution.P.observed.P.cel.Cel.rank); + Alcotest.(check string) "observed colour is white, overriding the season's own green" "white" + (Col.to_string resolution.P.observed.P.cel.Cel.colour); + Alcotest.(check bool) "observed subject is Bvm" true (resolution.P.observed.P.cel.Cel.subject = Sub.Bvm); + Alcotest.(check (list (pair string string))) "Holy Machabees still rides along as an ordinary commemoration" + [ ("holy-machabees", "Ordinary") ] + (List.map + (fun (c, p) -> (S.to_string c.P.cel.Cel.slug, match p with P.Privileged -> "Privileged" | P.Ordinary -> "Ordinary")) + resolution.P.commemorations); + Alcotest.(check int) "nothing omitted" 0 (List.length resolution.P.omitted) + +(* The other half of the SAME hazard: a real Saturday where RG 78's own + protasis is FALSE (a genuine sanctoral feast wins the day outright), so + the BVM-Saturday-shaped temporal candidate must lose, and be OMITTED under + RG 26 (a temporal Class4 non-vigil loser is never commemorated) exactly + like the plain ferial candidate it replaces always was -- proving [band]/ + [disposition] need no special case for this office at all, the design + claim the task report makes explicitly. 12 September 2026 is a Saturday + where `most-holy-name-of-mary` (data/ef/adjustments.sexp's own [Set_subject + Bvm] entry, Class3 Feast) wins outright -- ALSO tagged [subject = Bvm], + the one live data witness that could in principle confuse a + subject-keyed disposition rule with RG 26's own rank-keyed one: this test + proves they do NOT collide (RG 26 fires first, unconditionally, on the + loser's own [rank = Class4], never reaching a subject comparison at all). *) +let test_bvm_saturday_loses_to_a_real_feast_end_to_end () = + let date = mk 2026 9 12 in + let day_ctx = ctx date in + let bvm_saturday = of_temporal date in + let name_of_mary = + cand ~origin:P.Sanctoral ~rank:V.Class3 ~subject:Sub.Bvm ~layer:PE.universal_layer + "most-holy-name-of-mary" + in + let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ name_of_mary ] in + Alcotest.(check string) "Most Holy Name of Mary is observed, not the BVM Saturday office" + "most-holy-name-of-mary" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check (list string)) "nothing is commemorated" [] + (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations); + Alcotest.(check (list (pair string string))) + "the BVM-Saturday-shaped temporal candidate is omitted under RG26, not commemorated -- no Bvm-vs-Bvm \ + confusion with RG112(a)-style subject logic" + [ ("ef-time-after-pentecost-15-saturday", "omitted: yielded to a higher day") ] + (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) + +(* RG112(d), fix round 1 (coordinator finding F1) -- PE.marian_slugs's own + citation has the full argument. End-to-end through the REAL pipeline + (PE.band, PE.disposition AND PE.admit together): 16 July 2033, a real + otherwise-unoccupied Saturday, with the real data/ef/sanctoral.sexp + [our-lady-of-mt-carmel] entry as the day's only competing candidate. + Before this fix, [resolution.P.commemorations] held + [our-lady-of-mt-carmel:Ordinary] (confirmed live against real 2005/2033 + `colitur day` output before this fix existed); after, it is empty and + Mt Carmel is reported omitted, not silently dropped. *) +let test_bvm_saturday_excludes_mt_carmel_end_to_end () = + let date = mk 2033 7 16 in + let day_ctx = ctx date in + let bvm_saturday = of_temporal date in + let mt_carmel = + cand ~origin:P.Sanctoral ~rank:V.Class3 ~status:Cel.Commemoration_only ~subject:Sub.Saint + ~layer:PE.universal_layer "our-lady-of-mt-carmel" + in + let resolution = P.resolve real_rules day_ctx ~temporal:bvm_saturday ~sanctoral:[ mt_carmel ] in + Alcotest.(check string) "the BVM Saturday office is observed" + "ef-time-after-pentecost-5-saturday" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check (list string)) "Mt Carmel is NOT commemorated (RG112(d))" [] + (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.commemorations); + Alcotest.(check (list (pair string string))) "Mt Carmel is reported omitted, not silently dropped" + [ ("our-lady-of-mt-carmel", "omitted: yielded to a higher day") ] + (List.map (fun (c, reason) -> (S.to_string c.P.cel.Cel.slug, reason)) resolution.P.omitted) + (* RG 16(a) (register §6.0) reached end-to-end through the REAL pipeline ([PE.band], [PE.disposition] AND [PE.admit] together, not any one of them in isolation as this file's own hand-tagged rows test them @@ -1895,6 +2053,18 @@ let suite = "II-class Sunday override: RG109(b) witness admitted over an ordinary saint, end-to-end" `Quick test_ii_class_sunday_privileged_witness_admitted_end_to_end; Alcotest.test_case + "RG91 e27/RG78: the BVM Saturday office wins and commemorates Holy Machabees, end-to-end \ + (2026-08-01)" + `Quick test_bvm_saturday_wins_and_commemorates_end_to_end; + Alcotest.test_case + "RG91 e27/RG78/RG26: the BVM Saturday office loses to a real feast and is omitted, end-to-end \ + (2026-09-12)" + `Quick test_bvm_saturday_loses_to_a_real_feast_end_to_end; + Alcotest.test_case + "RG112(d), fix round 1 (F1): the BVM Saturday office excludes Mt Carmel, end-to-end \ + (2033-07-16)" + `Quick test_bvm_saturday_excludes_mt_carmel_end_to_end; + Alcotest.test_case "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; diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml index 5394173..4e97d33 100644 --- a/test/test_temporal_ef.ml +++ b/test/test_temporal_ef.ml @@ -296,13 +296,31 @@ let test_holy_name_sunday_shape () = compared against)" None (Colitur_kernel.Names.find (office (d 2026 1 4)).Cel.names en); (* Negative controls: every OTHER day in the 2-5 January window that year - stays [subject Temporal] -- the override is scoped to exactly the one - Sunday [T.holy_name_sunday] names, not the whole window. *) + is NOT Lord -- RG 17(a)'s own override is scoped to exactly the one + Sunday [T.holy_name_sunday] names, not the whole window. 2 and 5 January + (Friday, Monday) stay plain [Temporal] ferias, as before. 3 January is a + SATURDAY -- an unrelated rubric (RG 78, "De sancta Maria in sabbato", + {!bvm_saturday_names}'s own citation in temporal_ef.ml) legitimately + retags it [Bvm], not [Temporal]: it is an otherwise-unoccupied IV-class + Saturday (Christmastide's "ef-christmas-1-*" ferial branch, [ferial_rank]'s + unconditional Class4 there), so RG 78 fires regardless of RG 17(a) being + in play the same week -- the two rubrics are independent and this is not + a collision. This is deliberately NOT folded into the [Temporal] check + above: asserting "stays Temporal" for 3 January would be WRONG once RG + 78 is built, not merely incomplete. *) List.iter (fun dd -> Alcotest.(check bool) (Printf.sprintf "2026-01-%02d: NOT Lord (not the Sunday)" dd) true - (subject_of (d 2026 1 dd) = Sub.Temporal)) + (subject_of (d 2026 1 dd) <> Sub.Lord)) [ 2; 3; 5 ]; + Alcotest.(check bool) "2026-01-02: plain Temporal feria (not a Saturday)" true + (subject_of (d 2026 1 2) = Sub.Temporal); + Alcotest.(check bool) "2026-01-05: plain Temporal feria (not a Saturday)" true + (subject_of (d 2026 1 5) = Sub.Temporal); + Alcotest.(check bool) "2026-01-03: a Saturday -- RG 78's BVM Saturday office fires here \ + independently of RG 17(a)" + true + (subject_of (d 2026 1 3) = Sub.Bvm); (* 2027: 3 January is the Sunday (`date -d 2027-01-03 +%u` = 7) -- the oracle window's own second instance of the Sunday shape. missalemeum's 2027-01-03 row independently corroborates: rank 2, white, title "Holy @@ -489,14 +507,43 @@ let test_colours () = (6 Jan) is a Tuesday, so the actual first-Sunday-after-Epiphany origin is 11 Jan -- exercising all four sub-stretches of the fix. *) let test_christmastide_feria_slugs () = + (* 26 Dec 2026 is a SATURDAY -- but, fix round 1 (coordinator finding F2), + it can NEVER actually hold the BVM Saturday office in the real + resolved pipeline: 26/27/28 December are St Stephen/St John/the Holy + Innocents (RG 67-70), fixed, unconditional, real data/ef/sanctoral.sexp + entries every single year (rank >= Class2), which always outrank a + Class4 temporal candidate -- so RG 78's own protasis ("in quibus + OCCURRIT Officium de feria IV classis") is never satisfied by ANY day + in the 26-28 December stretch, in any year. [T.temporal] itself has no + visibility into the sanctoral layer at all (by design -- the temporal + candidate is computed independent of what saint's day it is; Precedence + decides the winner afterward), so it still unconditionally tags this + date [subject = Bvm] -- a genuine, correct fact about [T.temporal]'s + own pure computation, but NOT a claim that this shape is ever the real + OBSERVED office, and pinning it with RG-78 citation language as if it + were would overclaim. Only the SLUG is asserted here (unaffected + either way, unconditionally correct); the override demonstration + itself uses 3 January and 10 January below instead -- both, checked + directly against data/ef/sanctoral.sexp, have no competing entry in + any year, so the BVM Saturday office genuinely IS reachable there. *) Alcotest.(check string) "26 Dec (Sat)" "ef-christmas-0-saturday" (slug_of (d 2026 12 26)); Alcotest.(check string) "28 Dec (Mon)" "ef-christmas-0-monday" (slug_of (d 2026 12 28)); + (* 3 Jan 2026 is a SATURDAY, genuinely reachable (data/ef/sanctoral.sexp + has no 3 January entry) -- the real demonstration for the "-1-" + stretch that 26 December's own Saturday could not honestly provide. *) + Alcotest.(check string) "3 Jan (Sat): slug stays the ordinary \"-1-\" naming" "ef-christmas-1-saturday" + (slug_of (d 2026 1 3)); + Alcotest.(check string) "3 Jan (Sat): the BVM Saturday office overrides the season colour (white, RG 120(b))" + "white" (colour_of (d 2026 1 3)); + Alcotest.(check bool) "3 Jan (Sat): subject Bvm" true (subject_of (d 2026 1 3) = Sub.Bvm); Alcotest.(check string) "2 Jan (Fri)" "ef-christmas-1-friday" (slug_of (d 2026 1 2)); Alcotest.(check string) "5 Jan (Mon)" "ef-christmas-1-monday" (slug_of (d 2026 1 5)); (* 7-10 Jan precede the actual origin Sunday (11 Jan): colitur-only, not - lectio's "week 1" key, precisely to avoid colliding with 12-13 Jan below. *) + lectio's "week 1" key, precisely to avoid colliding with 12-13 Jan below. + 10 January is ALSO genuinely reachable (no sanctoral entry that date). *) Alcotest.(check string) "7 Jan (Wed, before the origin)" "ef-christmas-2-wednesday" (slug_of (d 2026 1 7)); Alcotest.(check string) "10 Jan (Sat, before the origin)" "ef-christmas-2-saturday" (slug_of (d 2026 1 10)); + Alcotest.(check bool) "10 Jan (Sat): subject Bvm" true (subject_of (d 2026 1 10) = Sub.Bvm); (* 12-13 Jan are on/after the origin: genuinely week 1, lectio's key. *) Alcotest.(check string) "12 Jan (Mon, on/after the origin)" "ef-time-after-epiphany-1-monday" (slug_of (d 2026 1 12)); @@ -813,6 +860,98 @@ let test_sacred_triduum_identity () = Alcotest.(check bool) "1666-04-22 Holy Thursday: subject Lord" true (subject_of (d 1666 4 22) = Sub.Lord) +(* RG 91 entry 27 / RG 78-79 (Caput IX, "De sancta Maria in sabbato") -- + {!Rite_ef.Temporal_ef.bvm_saturday_names}'s own citation has the full + primary-source argument (both photographic scans and the electronic + transcription for RG 78/79 themselves; both photographic scans for RG + 120(b)'s colour rule and the Missal's own "Missae de sancta Maria in + sabbato" heading). This test proves it holds across every season family + [ferial_rank] can give Class4 outside a privileged run -- Christmastide, + Septuagesima, Time after Epiphany, Time after Pentecost and ordinary + Paschaltide -- and that it does NOT fire where RG 78's own protasis is + false (Advent/Lent/Passiontide Saturdays, already II/III class; Ember + Saturdays, II/I class; a non-Saturday day in the very same week). Every + date below was independently checked against `colitur day <year>`'s own + real output (data/ef/sanctoral.sexp + adjustments.sexp) before being + pinned here, exactly like test_golden.ml's own discipline -- not merely + asserted against [T.temporal]'s own predicted shape a second time. *) +let test_bvm_saturday () = + (* Septuagesima, clean (no competing sanctoral candidate that year): + 19 February 2028, Septuagesima I Saturday. Colour is the clearest + possible witness here -- Septuagesima's own season colour is violet + (RG 128(b)), so a white result can only come from RG 120(b)'s override, + never from [season_colour] agreeing by coincidence the way + Christmastide/Paschaltide do below. *) + Alcotest.(check string) "2028-02-19 (Septuagesima I Saturday): slug stays the ordinary ferial key" + "ef-septuagesima-1-saturday" (slug_of (d 2028 2 19)); + Alcotest.(check string) "2028-02-19: class-4, unconditional (RG 91's own IV-class table position)" + "class-4" (rank_of (d 2028 2 19)); + Alcotest.(check string) "2028-02-19: white overrides Septuagesima's own violet (RG 120(b), not RG 128(b))" + "white" (colour_of (d 2028 2 19)); + Alcotest.(check bool) "2028-02-19: subject Bvm" true (subject_of (d 2028 2 19) = Sub.Bvm); + Alcotest.(check (option string)) "2028-02-19: Latin name, both photographic scans and the transcription, \ + word for word" + (Some "Officium sanctae Mariae in sabbato") (name_la_of (d 2028 2 19)); + (* Time after Epiphany, clean: 16 February 2030. Season colour there is + green (RG 127), like Time after Pentecost below -- a second, + independent witness that the override is season-blind, not merely + "true for Septuagesima's violet and Time after Pentecost's green + happen to share a fix". *) + Alcotest.(check string) "2030-02-16 (Time after Epiphany V Saturday): slug stays the ordinary ferial key" + "ef-time-after-epiphany-5-saturday" (slug_of (d 2030 2 16)); + Alcotest.(check string) "2030-02-16: white overrides Time after Epiphany's own green (RG 127)" "white" + (colour_of (d 2030 2 16)); + Alcotest.(check bool) "2030-02-16: subject Bvm" true (subject_of (d 2030 2 16) = Sub.Bvm); + (* Time after Pentecost, clean: 27 June 2026. *) + Alcotest.(check string) "2026-06-27 (Time after Pentecost IV Saturday): slug stays the ordinary ferial key" + "ef-time-after-pentecost-4-saturday" (slug_of (d 2026 6 27)); + Alcotest.(check string) "2026-06-27: white overrides Time after Pentecost's own green (RG 127)" "white" + (colour_of (d 2026 6 27)); + Alcotest.(check bool) "2026-06-27: subject Bvm" true (subject_of (d 2026 6 27) = Sub.Bvm); + (* Ordinary Paschaltide, clean, outside the privileged Easter octave: 18 + April 2026. Season colour there is ALREADY white (RG 119) -- colour + alone cannot distinguish the override firing from it simply not being + needed, so subject/name are the load-bearing assertions on this one, + not colour (the SAME reasoning [test_holy_family]'s own "Christmastide + already white" gap does not arise for -- this is a genuine, deliberate + exception recorded here, not an oversight). *) + Alcotest.(check string) "2026-04-18 (Paschaltide II Saturday): slug stays the ordinary ferial key" + "ef-easter-2-saturday" (slug_of (d 2026 4 18)); + Alcotest.(check string) "2026-04-18: white (coincides with Paschaltide's own RG 119 white -- not a \ + distinguishing witness on its own)" + "white" (colour_of (d 2026 4 18)); + Alcotest.(check bool) "2026-04-18: subject Bvm (the real witness this date proves, since colour alone \ + cannot)" + true (subject_of (d 2026 4 18) = Sub.Bvm); + Alcotest.(check (option string)) "2026-04-18: Latin name present too" (Some "Officium sanctae Mariae in sabbato") + (name_la_of (d 2026 4 18)); + (* Negative controls: RG 78's own protasis ("in quibus occurrit Officium + de feria IV classis") is false for these -- each is already II or III + class, never reaching Class4 at all, so none of them is overridden. *) + Alcotest.(check string) "2026-03-14 (an ordinary Lent Saturday): stays III class, violet, Temporal" + "class-3" (rank_of (d 2026 3 14)); + Alcotest.(check string) "2026-03-14: violet, NOT overridden (RG 78 never applies -- already III class)" + "violet" (colour_of (d 2026 3 14)); + Alcotest.(check bool) "2026-03-14: subject stays Temporal" true (subject_of (d 2026 3 14) = Sub.Temporal); + Alcotest.(check string) "2026-12-12 (an ordinary Advent Saturday, before 17 Dec): stays III class" + "class-3" (rank_of (d 2026 12 12)); + Alcotest.(check bool) "2026-12-12: subject stays Temporal (not overridden)" true + (subject_of (d 2026 12 12) = Sub.Temporal); + Alcotest.(check string) "2026-02-28 (Lent Ember Saturday): stays II class, NOT overridden" "class-2" + (rank_of (d 2026 2 28)); + Alcotest.(check bool) "2026-02-28: subject stays Temporal" true (subject_of (d 2026 2 28) = Sub.Temporal); + Alcotest.(check string) "2026-09-26 (September Ember Saturday): stays II class, NOT overridden" "class-2" + (rank_of (d 2026 9 26)); + (* A non-Saturday day in the SAME week as a clean BVM Saturday stays an + ordinary ferial -- the override is exactly one weekday wide, not the + whole week. 26 June 2026 is the Friday immediately before the 27 June + Saturday pinned above. *) + Alcotest.(check string) "2026-06-26 (Friday, day before a BVM Saturday): plain ferial, not overridden" + "ef-time-after-pentecost-4-friday" (slug_of (d 2026 6 26)); + Alcotest.(check string) "2026-06-26: green, the season's own colour, unaffected" "green" + (colour_of (d 2026 6 26)); + Alcotest.(check bool) "2026-06-26: subject stays Temporal" true (subject_of (d 2026 6 26) = Sub.Temporal) + 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, @@ -848,6 +987,7 @@ let suite_extra = Alcotest.test_case "colours" `Quick test_colours; Alcotest.test_case "sacred triduum identity (RG 91 entry 2, Gap 1)" `Quick test_sacred_triduum_identity; + Alcotest.test_case "BVM Saturday office (RG 91 entry 27, RG 78-79)" `Quick test_bvm_saturday; Alcotest.test_case "christmastide feria slugs" `Quick test_christmastide_feria_slugs; Alcotest.test_case "named days carry their week" `Quick test_named_days_carry_their_week; Alcotest.test_case "totality" `Quick test_totality; |
