summaryrefslogtreecommitdiff
path: root/test/test_oracle.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-17 12:28:03 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-17 12:28:03 +0200
commit8d1eb25a311e16ddb7bf89d2b21fb1ca0aa058a7 (patch)
tree9c72d1c30f0f37ef7138325abb3ca26ebe68ff14 /test/test_oracle.ml
parent9947a28f3a40bd8ac36892dcf5aad59d9e6f847c (diff)
downloadcolitur-8d1eb25a311e16ddb7bf89d2b21fb1ca0aa058a7.tar.gz
colitur-8d1eb25a311e16ddb7bf89d2b21fb1ca0aa058a7.zip
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.
Diffstat (limited to 'test/test_oracle.ml')
-rw-r--r--test/test_oracle.ml332
1 files changed, 320 insertions, 12 deletions
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