From 8d1eb25a311e16ddb7bf89d2b21fb1ca0aa058a7 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 17 Aug 2026 12:28:03 +0200 Subject: test(oracle): compare reading citations against missalemeum Layer 4 (test_oracle.ml) now extracts and compares the Epistle/Gospel citation for every one of the 730 days in the 2026-2027 fixture, the same axis Task 8 already added to layer 3. tools/ extract_missalemeum_oracle.py gained a structural citation-span detector (the first "*...*"-wrapped span under 40 chars containing a chapter:verse-shaped digit pair), verified exhaustively over all 1 458 Lectio/Evangelium sections. 728 of 730 days are comparable; the 2 that are not (Good Friday, both years) have no such section in missalemeum's own JSON at all -- a structural limit on the oracle's own side (M23), not a colitur gap. 451 of 730 days differ and are all explained across 19 cited entries; 0 unexplained. Chain step 4 (the Commons) has no layer-4 witness: confirmed empirically (a scratch sweep, not merely re-asserted from Task 6's own report) that none of the 15 Commons/proper saints is ever the observed office in this window. The comparison surfaced three genuine colitur bugs in data/ef/lectionary.sexp, all fixed at the source in tools/bootstrap_lectionary.ml: 1. Holy Name of Jesus (ef-holy-name-sunday/ef-holy-name) carried a mis-derived citation, borrowed by analogy from an unrelated ini section ("ef-christmas-sunday-0", the Sunday-within-the-Octave's own Mass) on the wrong assumption the two share a Mass. Disproved by missalemeum's own extraction for both real Holy Name Sundays in its window, confirmed against the Missal itself, twice, both scans (Acts 4:8-12/Luke 2:21, not Gal 4:1-7/Luke 2:33-40). 2. RG 17(a)'s own "diebus ferialibus" rubric (immediately following the Holy Name heading, both scans) was never implemented: 2-5 January ferias repeat 1 January's Mass, 7-12 January ferias before Holy Family Sunday repeat Epiphany's, and Holy Family's own week has a named formula ("Missa Dominicae Primae post Epiphaniam"). All three fell to step 3's undifferentiated Sunday walkback instead. Fixed for three 5-weekday families, deliberately excluding Saturday from all three -- RG 78's BVM Saturday Office wins there unconditionally, confirmed against missalemeum's own title on this window's own such Saturdays. 3. Corpus Christi and Sacred Heart (both real, never-impeded I-class movable feasts) had no lectionary entry at all. Both engines independently fell back to Trinity Sunday's own citation and silently agreed on the wrong answer -- the "Holy Thursday violet in both" shape CLAUDE.md names for exactly this reason. Because lectionary.sexp is shared with layer 3 (the lectio differential, 2005-2050), these fixes changed colitur's output for the whole domain, not only 2026-2027. data/ef/expected-divergences.sexp and test_differential.ml needed matching updates: C1 narrowed 361->173 and C25 narrowed 57->13 (rows that now resolve correctly moved to new entries), C16's note corrected (count unchanged), and six new entries added (C29-C34, 397 rows total, all verdict colitur). test_lectionary.ml and test_lectionary_ef.ml's own pins were updated to match (entry count 131->148; one step-3 regression test re-pointed to a date that still genuinely exercises that step). One entry, M26 (30 rows, verdict open), is a genuine unfixed gap: colitur's step-3 "resume the preceding Sunday" fallback is its only ferial-Mass mechanism, and missalemeum's own selection is more granular in three confirmed ways (a rotating votive Mass of the BVM, a named weekly formula for at least one week, and later Time-after-Pentecost weeks this task's own investigation did not narrow to a single rule) -- the same "needs a season-keyed selection mechanism, new behaviour not new data" shape data/ef/expected-divergences.sexp's own C6/C25 and Task 6's own residual concern already name, out of this task's own scope to build. dune test: 358 tests green. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force: 359 tests green, 89.5s. --- test/test_oracle.ml | 332 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 320 insertions(+), 12 deletions(-) (limited to 'test/test_oracle.ml') diff --git a/test/test_oracle.ml b/test/test_oracle.ml index f1ab9f1..41d8a2e 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -153,6 +153,7 @@ module Cel = Colitur_kernel.Celebration module Colour = Colitur_kernel.Colour module Names = Colitur_kernel.Names module Lang = Colitur_kernel.Lang +module Citation = Colitur_kernel.Citation module V = Rite_ef.Vocab_ef (* Same relative paths test_differential.ml uses: dune test runs from @@ -175,8 +176,12 @@ let allow_list_path = "../data/ef/expected-divergences-missalemeum.sexp" snapshot (fixtures/missalemeum-ef-2026-2027.provenance: still lectio commit d7da4b0, re-verified, first 8 fields byte-identical) -- only field 9 is new content, so this digest changes but the fixture's own data does - not. *) -let fixture_sha256 = "4162e79d7a0bc03b81a34ce6cd3b074e2d32570217759bc3b4058ada75ea8781" + not. + + UPDATED 2026-08-17, Task 9 (branch ef-lectionary): fields 10-11 + (first/gospel citations) appended, same snapshot, same provenance -- + first 9 fields re-verified byte-identical. *) +let fixture_sha256 = "350199498ce197c93dfee23bea62a47811995c3c70eec66707c123dd9e9c37fb" (* Identical technique to test_differential.ml's own [sha256_of_file] (that file's own comment explains why: shelling out to [sha256sum] @@ -252,6 +257,16 @@ type oracle_row = { o_commemorations : string list; o_commemoration_ids : string list; (** parallel to [o_commemorations], see field 9's own doc above *) o_displaced : string list; + o_first : string option; + (** Task 9, fields 10-11: the day's Epistle and Gospel citations, extracted + verbatim from missalemeum's own "Lectio"/"Evangelium" sections + (tools/extract_missalemeum_oracle.py's own header has the extraction + method and its exhaustive verification). [None] for the fixture's own + "-" sentinel -- the 2 Good Friday rows, which have no such section at + all (a multi-lesson "Missa Praesanctificatorum" structure instead), + the SAME shape colitur's own test_lectionary.ml already records for + its own hand-authored Holy Week data. *) + o_gospel : string option; } let explode s = List.init (String.length s) (String.get s) @@ -268,17 +283,29 @@ let id_rank id = let is_sancti_id id = String.length id >= 7 && String.sub id 0 7 = "sancti:" +(* Task 9: "-" -> [None], anything else -> [Some _] verbatim -- the same + sentinel convention test_differential.ml's own first/gospel columns + already use (that file's [decode_field]/[row] comment), here kept as an + [option] rather than a bare "-" string so a comparator bug cannot + accidentally compare two absent readings as if they were the SAME + present reading "-". *) +let opt_field f = if f = "-" then None else Some f + let oracle_row_of_line line = match String.split_on_char '|' line with - | [ date; rank; colours; title; _tempora; commemorations; displaced; _n_masses; commemoration_ids ] -> + | [ date; rank; colours; title; _tempora; commemorations; displaced; _n_masses; commemoration_ids; first; + gospel ] -> { o_date = date; o_rank = int_of_string rank; o_colours = explode colours; o_title = title; o_commemorations = split_list_field commemorations; o_commemoration_ids = split_list_field commemoration_ids; - o_displaced = split_list_field displaced } - | _ -> Alcotest.failf "malformed fixture line (expected 9 '|'-separated fields): %S" line + o_displaced = split_list_field displaced; + o_first = opt_field first; + o_gospel = opt_field gospel + } + | _ -> Alcotest.failf "malformed fixture line (expected 11 '|'-separated fields): %S" line let read_lines path = let ic = open_in path in @@ -324,6 +351,14 @@ type colitur_row = { applied to [LD.observed] instead of a commemoration candidate. *) c_commemorations : (string * int * Cel.status * string option) list; (** slug, rank, status, English name *) + c_first : string option; + (** Task 9: colitur's own resolved Epistle citation ({!Citation.First}), + the same [None]-for-absent convention as {!oracle_row}'s own + [o_first] -- read off [LD.citations] exactly the way + test_differential.ml's own [colitur_rows_2005_2050] already does + (that file's own [citation_ref], duplicated here rather than + shared, same reasoning as [real_layer]/[sha256_of_file] above). *) + c_gospel : string option; } let rank_to_int = function V.Class1 -> 1 | V.Class2 -> 2 | V.Class3 -> 3 | V.Class4 -> 4 @@ -360,13 +395,20 @@ let colitur_rows_2026_2027 () = (Slug.to_string c.Cel.slug, rank_to_int c.Cel.rank, c.Cel.status, Names.find c.Cel.names en)) day.LD.commemorations in + let citation_ref part = + match List.find_opt (fun (c : Citation.t) -> c.Citation.part = part) day.LD.citations with + | Some c -> Some c.Citation.reference + | None -> None + in rows := { 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 + c_commemorations = commemorations; + c_first = citation_ref Citation.First; + c_gospel = citation_ref Citation.Gospel } :: !rows | None -> Alcotest.failf "internal error: no resolved day for %s" (Date.to_iso8601 !d)); @@ -389,6 +431,10 @@ type field = | Comm_identity_unresolved | Observed_identity_mismatch | Observed_identity_unresolved + | First_mismatch + | First_unresolved + | Gospel_mismatch + | Gospel_unresolved let field_name = function | Rank -> "rank" @@ -399,6 +445,10 @@ let field_name = function | Comm_identity_unresolved -> "commemoration-identity-unresolved" | Observed_identity_mismatch -> "observed-identity-mismatch" | Observed_identity_unresolved -> "observed-identity-unresolved" + | First_mismatch -> "first-mismatch" + | First_unresolved -> "first-unresolved" + | Gospel_mismatch -> "gospel-mismatch" + | Gospel_unresolved -> "gospel-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 @@ -451,6 +501,92 @@ let identity_diff (c : colitur_row) (o : oracle_row) = let o_titles = List.sort compare o.o_commemorations in if c_titles = o_titles then None else Some Comm_identity_mismatch +(* Task 9: reading-citation comparison. colitur and missalemeum are both, + independently, English-abbreviated "Book chapter:verse-verse" citations + (unlike the commemoration/observed TITLE axes above, there is no + vocabulary gap to bridge here -- see tools/extract_missalemeum_oracle.py's + own header for the extraction, and data/ef/commons.sexp's own header for + colitur's side of the same convention) -- but the two sources punctuate + the identical reference differently often enough that a bare + [String.equal] would drown every genuine divergence in typographic noise: + a trailing full stop missalemeum's own source sentence happens to end + with ("1 Cor 1:17-25." vs colitur's "1 Cor. 1:17-25"), an abbreviation dot + after the book name (present on one side, absent on the other, with no + consistent rule EITHER side follows -- missalemeum alone has both "1 Cor" + and "1 Cor." in its own fixture), and a chapter/verse separator that is + sometimes "." instead of ":" ("John 20. 19-31"). [normalize_citation] + collapses exactly these three -- PURE typography, never the substantive + book/chapter/verse content -- so the identity axis below compares what + the reference actually IS, not how either source happened to punctuate + it that day. Order matters: the digit-period-digit rewrite (chapter.verse + -> chapter:verse) runs BEFORE the letter-period strip, so "20. 19-31" + becomes "20:19-31" before the (now absent) letter-period pass would have + nothing left to touch it. *) +let normalize_citation s = + let s = String.trim s in + let buf = Buffer.create (String.length s) in + String.iter (fun c -> if c = '\t' || c = '\n' then Buffer.add_char buf ' ' else Buffer.add_char buf c) s; + let s = Buffer.contents buf in + (* Collapse runs of whitespace to one space. *) + let s = + let b = Buffer.create (String.length s) in + let prev_space = ref false in + String.iter + (fun c -> + if c = ' ' then ( + if not !prev_space then Buffer.add_char b ' '; + prev_space := true) + else ( + Buffer.add_char b c; + prev_space := false)) + s + |> fun () -> Buffer.contents b + in + (* digit "." (optional space) digit -> digit ":" digit (the chapter/verse + separator seen as "." on missalemeum's side, e.g. "John 20. 19-31"). *) + let is_digit c = c >= '0' && c <= '9' in + let n = String.length s in + let b = Buffer.create n in + let i = ref 0 in + while !i < n do + let c = s.[!i] in + if c = '.' && !i > 0 && is_digit s.[!i - 1] then ( + let j = ref (!i + 1) in + if !j < n && s.[!j] = ' ' then incr j; + if !j < n && is_digit s.[!j] then ( + Buffer.add_char b ':'; + i := !j) + else ( + Buffer.add_char b c; + incr i)) + else ( + Buffer.add_char b c; + incr i) + done; + let s = Buffer.contents b in + (* Any remaining "." is either an abbreviation dot (letter, e.g. "Cor.") + or a bare trailing full stop -- neither carries reference content, so + both are simply dropped. *) + let b = Buffer.create (String.length s) in + String.iter (fun c -> if c <> '.' then Buffer.add_char b c) s; + String.trim (Buffer.contents b) + +(* [None] (unresolved) is reported separately from a genuine mismatch + between two present values -- the same [Comm_identity_unresolved] / + [Comm_identity_mismatch] split above, applied to a single reference + string instead of a multiset of titles. The ONLY unresolved case either + side of this whole fixture ever produces is missalemeum's own 2 Good + Friday rows (extract_missalemeum_oracle.py's own header); colitur itself + never emits [None] for First/Gospel anywhere in this window (Task 8: + "domain-wide there are now ZERO days with empty citations"), so this is + the FIRST time a [_unresolved] outcome in this file can fire on the + colitur side too, in principle, and both are covered. *) +let citation_diff ~mismatch ~unresolved (c : string option) (o : string option) = + match (c, o) with + | None, None -> None + | (None, Some _ | Some _, None) -> Some unresolved + | Some cs, Some os -> if String.equal (normalize_citation cs) (normalize_citation os) then None else Some mismatch + let diff_fields (c : colitur_row) (o : oracle_row) = let c_has = c.c_commemorations <> [] and o_has = o.o_commemorations <> [] in List.filter_map @@ -466,7 +602,9 @@ let diff_fields (c : colitur_row) (o : oracle_row) = Some Comm_count else None); identity_diff c o; - observed_identity_diff c o + observed_identity_diff c o; + citation_diff ~mismatch:First_mismatch ~unresolved:First_unresolved c.c_first o.o_first; + citation_diff ~mismatch:Gospel_mismatch ~unresolved:Gospel_unresolved c.c_gospel o.o_gospel ] (* ---------------------------------------------------------------------- *) @@ -976,6 +1114,137 @@ let m22_commemoration_matches (c : colitur_row) = | [ a; b ] -> (String.equal a "barbara") <> (String.equal b "barbara") | _ -> false +(* M23 -- Task 9 (branch ef-lectionary, layer-4 oracle): Good Friday, both + years. missalemeum's own JSON has no "Lectio"/"Evangelium" section at + all for this one day (a multi-lesson "Missa Praesanctificatorum" + structure instead, "Lectiones"/"Passio", the SAME shape colitur's own + Holy Week data hit at bootstrap time -- tools/ + extract_missalemeum_oracle.py's own header has the extraction detail) + -- a comparator-structural absence on the ORACLE side, the mirror image + of [Comm_identity_unresolved]'s own colitur-side absence, not a rubric + dispute: colitur's own citation (Ex 12:1-11/John 18:1-40; 19:1-42, + tools/bootstrap_lectionary.ml's own [holy_week_entries], Missal-verified + twice) is correct and complete, missalemeum's fixture simply has nothing + comparable to check it against. *) +let m23_dates = [ "2026-04-03"; "2027-03-26" ] + +(* M24 -- Task 9: four Holy Week rows where missalemeum's own Gospel + citation embeds a LATIN book abbreviation ("Luc"/"Joann") instead of its + own usual English one ("Luke"/"John") -- checked directly against the + raw JSON, not assumed: every OTHER citation in the fixture, including + these same four rows' own Epistle, uses English throughout. A source + presentation quirk on missalemeum's own side (its English-translation + layer appears to have a template gap for these four Gospel citations + specifically), not a different Scripture reference -- the same + underlying verses, same chapter/verse numbers, confirmed identical + after every OTHER normalisation this file's own [normalize_citation] + already applies. Not folded into that function (a two-entry Latin/ + English book-name table would be the first step of a general + translation layer this project's own citation discipline deliberately + does not build -- the SAME limit M5 already names for a different + vocabulary gap) -- named here instead, narrowly, as what it is. *) +let m24_dates = [ "2026-04-01"; "2026-04-02"; "2027-03-24"; "2027-03-25" ] + +(* M25 -- Task 9: four Lenten rows where the underlying verse range is + IDENTICAL but the two sources punctuate its own internal boundary + differently -- "4 Kings 5:1-15" (colitur) vs "4 Kings, 5:1-15" + (missalemeum, a spurious comma after the book name with no citation + content of its own) on 2026-03-09/2027-03-01; "Dan 14:27, 28-42" + (colitur, verse 27 named separately then 28-42) vs "Dan 14:27-42" + (missalemeum, the identical span as one contiguous range) on + 2026-03-24/2027-03-16. Neither is a book/chapter/verse-NUMBER + difference [normalize_citation] could safely absorb without also + risking silently equating two GENUINELY different verse lists + elsewhere in the fixture (e.g. a real "12, 15-20" vs "12-20" would + look identical to this rule but mean different things) -- named + individually instead, the same discipline M5's own identity-gated + predicate already applies to a citation-vocabulary gap. *) +let m25_dates = [ "2026-03-09"; "2026-03-24"; "2027-03-01"; "2027-03-16" ] + +(* M26 -- Task 9: the large remaining population, one single root cause. + {!Rite_ef.Lectionary_ef.readings}'s own header comment already names + this LIMIT, not a rubric dispute this task settled: step 3 ("a feria + with no proper says the preceding SUNDAY's Mass") is colitur's only + fallback mechanism, and its own WARRANT is "lectio's own observed + behaviour... not a confirmed Missal citation" -- register's own + standing note. missalemeum's own ferial-Mass selection is more + granular than that single rule in (at least) three distinct ways this + task's own investigation found and confirmed against the primary + source, but did NOT build (a genuine, understood, ALREADY-DOCUMENTED + architecture gap, the SAME shape data/ef/expected-divergences.sexp's + own C6/C25 entries already carry for the sibling lectio differential, + and Task 6's own residual concern 4/5, "the BVM Saturday Office still + emits its feria's Mass... needs... a season-keyed selection among five + formularies" -- RG 309(a), "iuxta temporum diversitatem"): + (1) a rotating VOTIVE MASS OF THE BVM ("Missae de sanctae Mariae in + sabbato" I-V and their Time-after-Pentecost siblings, missalemeum's own + "II/III/V Mass of the B. V. M." titles) on an otherwise-unoccupied + feria, not only a Saturday -- 2026-01-03/2026-01-10/2027-01-02/ + 2027-01-09 are RG 78's own BVM SATURDAY Office specifically (Task 6's + own named open item; colitur's own step-3 fallback, not the votive + Mass, is what these four rows actually show -- 2026-01-03/2027-01-09 + are `ef-christmas-1-saturday`, C25's own unchanged population in the + sibling lectio differential; 2026-01-10/2027-01-02 are + `ef-christmas-2-saturday`); the rest of this entry's own population + are ordinary weekday ferias in Time after Pentecost showing the SAME + votive-Mass title; + (2) a NAMED weekly formula distinct from the plain Sunday-repeat rule + for at least one week this task's own primary-source reading found by + accident while fixing a DIFFERENT gap (Holy Family's own week, + "Missa Dominicae Primae post Epiphaniam" -- built, tools/ + bootstrap_lectionary.ml's own [epiphanytide_opening_entries]) -- + 2026-06-02/03 and 2027-05-24 are the SAME shape for a different week + (the ferias between Trinity Sunday and Corpus Christi, "Missa + Dominicae I post Pentecosten", 1 John 4:8-21/Luke 6:36-42, visible in + the raw scan while researching Corpus Christi's own Mass -- see + docs/research/scan1.txt around line 21800 -- but NOT independently + verified as a general "every week has its own named formula" rule the + way the January window was, and NOT built here); + (3) later Time-after-Pentecost weeks whose own votive/ferial answer + this task's investigation did not narrow to a single citable rule at + all (2026-10-26 through 2026-11-28, 2027-11-03 through 2027-11-27). + ALL THREE are variations on the identical root limitation, not three + separate bugs, and none is safe to fix piecemeal inside this task's own + scope (extending layer 4 + triage) without the same real, dedicated, + TDD/mutation-tested unit of work C6/C25 and Task 6's own open item + already call for -- a season-keyed ferial/votive-Mass-selection + mechanism, new behaviour, not new data. Verdict open, the SAME fourth + verdict M15/M18 already establish for a genuine comparator/architecture + LIMIT, honestly counted rather than silently absorbed or left failing + without explanation. A literal date list, not a shape predicate (no + single structural signal -- unlike C17's own [subject = bvm] -- exists + to distinguish this population from a genuinely new, unrelated + citation bug on some other Time-after-Pentecost feria): the SAME + discipline this file's own header names for every other entry here. *) +let m26_dates = + [ "2026-01-03"; "2026-01-10"; "2026-02-14"; "2026-06-02"; "2026-06-03"; "2026-06-20"; "2026-06-27"; + "2026-07-04"; + "2026-07-11"; "2026-08-01"; "2026-10-26"; "2026-10-27"; "2026-10-29"; "2026-10-30"; "2026-10-31"; + "2026-11-07"; "2026-11-28"; "2027-01-02"; "2027-01-09"; "2027-05-24"; "2027-07-17"; "2027-07-24"; + "2027-09-04"; "2027-09-11"; "2027-10-30"; "2027-11-03"; "2027-11-05"; "2027-11-06"; "2027-11-13"; + "2027-11-27" ] + +(* M27 -- Task 9: Christmas Day, both years. Christmas has THREE Masses in + the 1962 Missal (Midnight/"in nocte", Dawn/"in aurora", Day/"in die"), + each with its own distinct Epistle+Gospel -- missalemeum's own JSON + array has all three, but tools/extract_missalemeum_oracle.py's own + convention (documented in its header, verified for every OTHER field + this fixture reads) takes entry[0] uniformly, which for THIS one day is + the Midnight Mass (Titus 2:11-15/Luke 2:1-14) -- colitur's own citation + (Heb 1:1-12/John 1:1-14, bootstrapped from lectio, matching the + Day Mass, the conventional "default" answer to "what does 25 December + read") is a DIFFERENT, also-genuine Mass of the identical day, not a + wrong one. A comparator/fixture-extraction artefact (which of several + equally-real Masses a multi-Mass day's own entry[0] happens to be), the + SAME limit family as M23's own oracle-side absence -- not a rubric + dispute, and not fixed by changing the extractor's own array-index + convention, which the extraction script's own header already + documents as deliberate and verified for every OTHER field this + fixture reads (rank/colours/tempora/commemorations/displaced, all + confirmed IDENTICAL across a day's own multiple Masses -- only the + citation fields are not, discovered by this task). *) +let m27_dates = [ "2026-12-25"; "2027-12-25" ] + (* [_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, @@ -987,9 +1256,17 @@ let layer_m_reason (c : colitur_row) (_o : oracle_row) diffs = 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. *) + widened entries below share, restated for this one's own shape. + WIDENED AGAIN, Task 9 (branch ef-lectionary, layer-4 oracle): + [First_mismatch]/[Gospel_mismatch] join the accepted set -- once the + OBSERVED day itself differs (the vigil vs the Sunday RG 33 requires), + its own reading citation is a natural CONSEQUENCE of that same + identity divergence, not a second, independent cause (the same + "identity already differs, so citation differs too" reasoning + test_differential.ml's own C14/C15/C16/C29 already establish). *) else if List.mem c.c_date m1_dates - && subset diffs [ Rank; Colour_f; Comm_presence; Observed_identity_unresolved ] + && subset diffs + [ Rank; Colour_f; Comm_presence; Observed_identity_unresolved; First_mismatch; Gospel_mismatch ] then Some "M1" (* 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 @@ -1014,8 +1291,18 @@ let layer_m_reason (c : colitur_row) (_o : oracle_row) diffs = 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. *) + (* WIDENED AGAIN, Task 9 (branch ef-lectionary, layer-4 oracle): + [First_mismatch]/[Gospel_mismatch] join the accepted set -- colitur's + OBSERVED day here is Joseph, whose own citations differ from + missalemeum's own Seven-Sorrows-of-Passion-Friday citation for the + identical, already-named reason (the Seven Sorrows commemoration is + never constructed, so colitur observes Joseph outright instead), not + a fourth, independent cause. *) else if List.mem c.c_date m13_dates - && subset diffs [ Rank; Colour_f; Comm_identity_unresolved; Observed_identity_mismatch ] + && subset diffs + [ Rank; Colour_f; Comm_identity_unresolved; Observed_identity_mismatch; First_mismatch; + Gospel_mismatch + ] then Some "M13" else if List.mem c.c_date m15_dates && diffs = [ Comm_identity_unresolved ] then Some "M15" (* M16's own subset widened (this task): colitur's observed celebration @@ -1040,6 +1327,22 @@ let layer_m_reason (c : colitur_row) (_o : oracle_row) diffs = && subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ] && m20_commemoration_matches c then Some "M20" + else if + List.mem c.c_date m23_dates + && subset diffs [ Observed_identity_unresolved; First_unresolved; Gospel_unresolved ] + then Some "M23" + else if List.mem c.c_date m24_dates && subset diffs [ Observed_identity_unresolved; First_mismatch; Gospel_mismatch ] + then Some "M24" + else if List.mem c.c_date m25_dates && subset diffs [ Observed_identity_unresolved; First_mismatch ] then + Some "M25" + else if + List.mem c.c_date m26_dates + && subset diffs [ Observed_identity_unresolved; First_mismatch; Gospel_mismatch ] + then Some "M26" + else if + List.mem c.c_date m27_dates + && subset diffs [ Observed_identity_unresolved; First_mismatch; Gospel_mismatch ] + then Some "M27" else None (* ---------------------------------------------------------------------- *) @@ -1089,19 +1392,24 @@ let describe_comm (slug, rank, status, name) = Printf.sprintf "%s(rank=%d,status=%s,name=%s)" slug rank (status_to_string status) (match name with Some n -> Printf.sprintf "%S" n | None -> "NONE") +let describe_opt = function Some s -> Printf.sprintf "%S" s | None -> "NONE" + let describe_unexplained (c : colitur_row) (o : oracle_row) diffs = - Printf.sprintf "%s: %s differ -- colitur=(rank=%d colour=%c comms=[%s]) oracle=(rank=%d colours=[%s] \ - title=%S comms=[%s] comm_ids=[%s] displaced=[%s])" + Printf.sprintf "%s: %s differ -- colitur=(rank=%d colour=%c comms=[%s] first=%s gospel=%s) \ + oracle=(rank=%d colours=[%s] title=%S comms=[%s] comm_ids=[%s] displaced=[%s] first=%s \ + gospel=%s)" c.c_date (String.concat "," (List.map field_name diffs)) c.c_rank c.c_colour (String.concat ";" (List.map describe_comm c.c_commemorations)) + (describe_opt c.c_first) (describe_opt c.c_gospel) o.o_rank (String.concat "" (List.map (String.make 1) o.o_colours)) o.o_title (String.concat ";" o.o_commemorations) (String.concat ";" o.o_commemoration_ids) (String.concat ";" o.o_displaced) + (describe_opt o.o_first) (describe_opt o.o_gospel) let test_fixture_checksum () = Alcotest.(check string) "fixture SHA-256 matches its provenance note" fixture_sha256 -- cgit v1.3