diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-15 02:58:00 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-15 02:58:00 +0200 |
| commit | 459d2c74711e6a152fb338f88d2327f12180b61f (patch) | |
| tree | 0151a76e7157b727457e39ba4622feff8faea3da /test/test_differential.ml | |
| parent | 6b41415f17f3264454f6dcc13898948fd1902d0b (diff) | |
| download | colitur-459d2c74711e6a152fb338f88d2327f12180b61f.tar.gz colitur-459d2c74711e6a152fb338f88d2327f12180b61f.zip | |
test(differential): compare reading citations, 16 801 days
Layer 3 is NOT blind to the ferial resumption rule, unusually for this
project: lectio implements the same rubric independently, so the differential
checks colitur's rule against a second implementation rather than against
silence.
It IS blind to the Commons -- lectio has no such concept -- so chain step 4
rests on layer 4 alone.
Divergence triage, 1 532 of 16 801 days differ (15 269 match exactly),
0 unexplained:
- A genuine colitur bug found and fixed: data/ef/lectionary.sexp carried
lectio's own ini section names verbatim, never translated into the slugs
Temporal_ef actually computes for the identical office. Data the bootstrap
already held (Passiontide both weeks, the Pentecost Ember days, Holy Name
Sunday) was unreachable by chain step 2, silently falling through to a
wrong step-3 answer -- invisible until citation content became comparable.
tools/bootstrap_lectionary.ml gained colitur_keys (rename/widen), a
hand-authored ef-nativity-vigil entry cross-sourced from lectio's own
sanctoral calendar and corroborated by colitur's own (overlay-suppressed)
sanctoral bootstrap of the same source, and a derived ef-holy-name entry
(RG 17(a)'s single Mass heading covers both the Sunday and 2-January
shapes). One guess (giving the fixed Nativity-Octave days Holy Name
Sunday's own citation uniformly) was tried and reverted once measured
against the fixture -- Missal-verified to be right only for the genuine
"Sunday within the Octave" case, wrong whenever the true preceding Sunday
is Christmas Day or Advent IV instead.
- 10 existing/widened Layer C entries (C1/C6/C8/C14/C15/C16/C17/C18 --
C6 and C17 narrowed with the removed population accounted for, not
silently re-absorbed) plus two new ones: C19 (the already-adjudicated
Time-after-Epiphany week-numbering offset, now visible in citation
content), C20 (lectio computes no reading at all for the seven
readingless saints Task 6 gave a Common or proper -- colitur is more
complete, not wrong).
- One entry, C21, is a genuine colitur LOGIC limitation found but not
fixed: Lectionary_ef.readings' step 3 is a single hop to the preceding
Sunday, insufficient for a run of colitur-only fixed Christmastide slugs
whose own preceding Sunday is itself another entry-less fixed slug.
Recorded verdict open (data/ef/expected-divergences-missalemeum.sexp's
own established precedent for this shape), not left silently failing --
fully diagnosed in task-8-report.md, recommended as a dedicated
follow-up task rather than a hasty fix to a shared, multi-round-reviewed
kernel-adjacent function.
Diffstat (limited to 'test/test_differential.ml')
| -rw-r--r-- | test/test_differential.ml | 232 |
1 files changed, 218 insertions, 14 deletions
diff --git a/test/test_differential.ml b/test/test_differential.ml index acbb559..74cc54d 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -17,8 +17,12 @@ comment says so), not an RG 111 admitted set -- lectio has no RG 111 admission logic at all. Comparing that column would compare colitur's real admitted commemorations against lectio's rejects, which proves - nothing. Only the seven leading columns (date weekday season week slug - rank colour) are read from either stream. + nothing. Of the eleven fixed columns the fixture's own dumper now + prints, nine are read from either stream: the original seven (date + weekday season week slug rank colour) plus, since Task 8, the two + reading-citation columns (first, gospel) -- name_en/name_pl (the + remaining two) are parsed and immediately discarded, never compared + (this file's own [row_of_line] comment says why). 2. lectio's own EF oracle test (~/git/projects/lectio, internal/calendar/oracle_ef_test.go) asserts ONLY season, strictly, @@ -139,6 +143,7 @@ module Date = Colitur_kernel.Date module Cel = Colitur_kernel.Celebration module Colour = Colitur_kernel.Colour module Subject = Colitur_kernel.Subject +module Citation = Colitur_kernel.Citation module V = Rite_ef.Vocab_ef (* Same relative paths test_rite_ef.ml/test_sanctoral_ef.ml use: dune test @@ -159,7 +164,7 @@ let allow_list_path = "../data/ef/expected-divergences.sexp" deliberately, after re-running the exact command the provenance note names -- never by copying the actual value back in to make a mismatch pass, which would defeat the point of pinning it at all. *) -let fixture_sha256 = "fc4832f1bc1bdfd322e808ae4929487628981792c23f959c1764c8eaedbfaf7f" +let fixture_sha256 = "8c0b9d9fc284138e69dbbbd87e9153a17f1abb69a39928464d83d248a819f376" (* Same technique tools/bootstrap_sanctoral.ml already uses for this exact purpose (that file's own comment: shelling out to the system's @@ -244,6 +249,16 @@ type row = { COLITUR row [c], never [l] -- the same asymmetry [row]'s other fields do not have, called out here rather than left implicit. *) subject : string; + (* Task 8: the day's resolved Epistle/Lesson and Gospel citations, ALREADY + DECODED (lectio's own "_"-for-space convention reversed at parse time, + [decode_field] below; colitur's own [Citation.reference] never contains + an underscore in the first place, so decoding it is a no-op) -- both + sides therefore carry the same "-" sentinel for "no reading resolved" + that the fixture's own name/week columns already use, so a plain + [String.equal] is the whole comparison, matching every other field in + this record. *) + first : string; + gospel : string; } let read_lines path = @@ -257,11 +272,32 @@ let read_lines path = in loop [] +(* Reverses the fixture's own "_" -for-space, "-" -for-absent encoding + (shared by the name_en/name_pl columns and, since Task 7's + lectio-ef-dump refresh, by first/gospel too -- see the fixture's own + .provenance note). Verified against the real file, not assumed: a + citation like ["Rom_13:11-14"] round-trips to ["Rom 13:11-14"], and the + literal sentinel ["-"] (no underscore in it) passes through unchanged, + so one function serves both the "real value" and the "absent" case + without a special-cased branch. *) +let decode_field s = String.map (fun c -> if c = '_' then ' ' else c) s + 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; subject = "-" } - | _ -> Alcotest.failf "malformed fixture line (fewer than 7 fields): %S" line + | date :: weekday :: season :: week :: slug :: rank :: colour :: _name_en :: _name_pl :: first + :: gospel :: _others -> + { date; + weekday; + season; + week; + slug; + rank; + colour; + subject = "-"; + first = decode_field first; + gospel = decode_field gospel + } + | _ -> Alcotest.failf "malformed fixture line (fewer than 11 fields): %S" line let lectio_rows () = List.map row_of_line (read_lines fixture_path) @@ -293,6 +329,19 @@ let colitur_rows_2005_2050 () = let week = match t.Colitur_kernel.Temporal.week with Some n -> string_of_int n | None -> "-" in + (* [Rite.readings] already resolved the day's citations (calendar.ml's + own [build_day]); this just projects the ONE reading of each + PART lectio's own dumper prints ("first"/"gospel" -- neither + engine's EF stream carries Psalm/Second/Tract/etc, task-7-report.md's + own note on why). "-" for a part with no resolved reading, the + same sentinel [row_of_line]'s [decode_field] leaves an absent + lectio field as, so the two sides compare with a plain + [String.equal] like every other field here. *) + let citation_ref part = + match List.find_opt (fun (c : Citation.t) -> c.Citation.part = part) day.LD.citations with + | Some c -> c.Citation.reference + | None -> "-" + in rows := { date = Date.to_iso8601 day.LD.date; weekday = Date.weekday_to_string t.Colitur_kernel.Temporal.weekday; @@ -301,7 +350,9 @@ let colitur_rows_2005_2050 () = slug = Slug.to_string cel.Cel.slug; rank = V.rank_to_string cel.Cel.rank; colour = Colour.to_string cel.Cel.colour; - subject = Subject.to_string cel.Cel.subject + subject = Subject.to_string cel.Cel.subject; + first = citation_ref Citation.First; + gospel = citation_ref Citation.Gospel } :: !rows | None -> Alcotest.failf "internal error: no resolved day for %s" (Date.to_iso8601 !d)); @@ -405,13 +456,23 @@ let strip_epiphany_index slug = (* ---------------------------------------------------------------------- *) (* Field-diff computation: applies Layers A and B, then reports exactly *) -(* which of the FIVE substantive columns still differ (week is never *) +(* which of the SEVEN substantive columns still differ (week is never *) (* inspected at all -- limit 3). weekday is included defensively: dates *) (* are checked 1:1 aligned before this runs, so it should never fire, and *) (* if it ever does that is real signal, not noise to normalise away. *) +(* *) +(* Task 8: First_f/Gospel_f join the SAME diff set the other five fields *) +(* share -- deliberately, not a separate pass gated on "only when the day *) +(* otherwise matches". A citation difference on a day whose identity *) +(* ALSO differs is not a second, independent fact needing its own *) +(* citation -- it is the same divergence Layer C already explains *) +(* (different office observed, therefore a different Mass read); each *) +(* Layer C predicate below is widened to accept First_f/Gospel_f *) +(* alongside its own existing field set wherever that is what the real *) +(* fixture data shows, never blanket-permitted. *) (* ---------------------------------------------------------------------- *) -type field = Weekday | Season | Slug_f | Rank | Colour_f +type field = Weekday | Season | Slug_f | Rank | Colour_f | First_f | Gospel_f let field_name = function | Weekday -> "weekday" @@ -419,6 +480,8 @@ let field_name = function | Slug_f -> "slug" | Rank -> "rank" | Colour_f -> "colour" + | First_f -> "first" + | Gospel_f -> "gospel" let month_of_date date = int_of_string (String.sub date 5 2) @@ -433,6 +496,8 @@ let diff_fields (l : row) (c : row) = (if String.equal l_season c.season then None else Some Season); (if String.equal l_slug c_slug then None else Some Slug_f); (if String.equal l.rank c.rank then None else Some Rank); + (if String.equal l.first c.first then None else Some First_f); + (if String.equal l.gospel c.gospel then None else Some Gospel_f); (if String.equal l.colour c.colour then None else Some Colour_f) ] @@ -605,6 +670,102 @@ let is_bvm_saturday_row (c : row) diffs = diffs = [ Colour_f ] && String.equal c.weekday "saturday" && String.equal c.rank "class-4" && String.equal c.colour "white" && String.equal c.subject "bvm" +(* C19 (task 8, branch ef-lectionary): the Time-after-Epiphany WEEK-INDEX + offset (limit 3 in this file's own header comment, [strip_epiphany_index] + above) is not merely a slug-digit cosmetic difference -- it is a real + week-COUNT divergence between the two engines for the REST of the season + whenever Holy Family (RG 17(b)) has just displaced the fixed Baptism + commemoration (the same 7 years C15 above names): colitur's own week + numbering runs one week "behind" lectio's until Time after Epiphany ends + at Septuagesima (which re-synchronises both engines' counts from zero), + so the SAME calendar day's Sunday-derived reading (whichever Sunday's + Mass a feria repeats, or a Sunday's own Mass) is drawn from two + adjacent-but-different weeks on each side -- a citation divergence with + NO slug divergence at all (Layer B's own [strip_epiphany_index] already + normalises the embedded digit away, correctly: the office FAMILY is the + same, only which week's content is read differs). Confirmed empirically, + not assumed: every occurrence across the whole 2005-2050 fixture falls in + exactly these 7 years, and only on a [c.slug] in the + [ef-time-after-epiphany-] family -- checked, not merely gated, so a real + bug on an unrelated slug in one of these years still surfaces as + unexplained. [Colour_f] joins the accepted set for the Saturdays that + ALSO happen to carry C17's own BVM-Saturday office that week -- a second, + independent, already-cited cause layering onto the SAME row, not + something this predicate explains on its own; C17's own 416-row count is + untouched, since its own guard still requires diffs = EXACTLY + [Colour_f], which these rows (carrying [First_f]/[Gospel_f] too) never + satisfy. *) +let epiphany_week_shift_years_2005_2050 = [ 2008; 2013; 2019; 2030; 2036; 2041; 2047 ] + +let year_of_date date = int_of_string (String.sub date 0 4) + +(* C20 (task 8): lectio genuinely computes NO reading at all -- "-"/"-" on + both parts -- for 14 civil days across 2005-2050, all seven of the + readingless class-3 saints Task 6 gave a proper or a Common + (data/ef/commons.sexp's own "OBSERVABILITY" note, task-6-report.md ยง1): + colitur is MORE COMPLETE here, not wrong -- every one of these 14 answers + is a direct Missal citation (data/ef/adjustments.sexp's own propers for + thomas-aquinas/john-of-god/francis-of-paola, or data/ef/commons.sexp's + own Common assignment for the other four), each read from three + independent witnesses (task-6-report.md's own account of its own + cross-check discipline), not invented. Gated on the literal 7-slug list + (the same identity-pinning discipline every other entry here uses) AND + on lectio's own field literally reading "-"/"-" -- never "any + first/gospel mismatch on these slugs", which would also swallow a + genuine future content disagreement on one of these same seven saints. *) +let lectio_no_reading_slugs = + [ "vincent-ferrer"; "francis-of-paola"; "isidore-of-seville"; "thomas-aquinas"; "john-of-god"; + "sts-felicitas-perpetua"; "frances-rome" ] + +(* C21 (task 8) -- [verdict open], the FIRST entry in this file that is not + [verdict colitur] (see data/ef/expected-divergences.sexp's own header for + the file-level rule this departs from, and the note on why). A genuine, + precisely-diagnosed colitur LOGIC LIMITATION, not a data/bootstrap issue + and not a rubric dispute: {!Rite_ef.Lectionary_ef.readings}'s step 3 is a + SINGLE hop to the immediately preceding civil Sunday's own temporal + identity. For a run of colitur-only FIXED Christmastide slugs + ([ef-nativity-octave-day-{5,6,7}], 29-31 December; the + [ef-christmas-{0,1,2}-<weekday>] ferias of the two weeks that follow) that + ONE Sunday is itself frequently ANOTHER entry-less fixed slug (another + Nativity-Octave day, or, on the single occasion 24 December itself falls + on a Sunday, [ef-nativity-vigil]) -- so step 3 finds nothing (95 of these + 107 rows) or, on the 12 rows where the walk happens to land exactly on + that one Vigil-is-a-Sunday collision, finds the WRONG thing (the Vigil's + own Mass, misapplied to a later feria that is not itself Christmas Eve). + lectio's own resolution reaches further by some mechanism this task did + not fully reverse-engineer (empirically: it variously finds Christmas + Day's own Mass or Advent IV's own Mass, depending on the year's + alignment). Properly fixing this needs either an iterative walk-back in + [readings] itself or a per-year-verified direct entry for every fixed + Christmastide slug (task 6's own rigor, applied to a new population) -- + both are logic/behaviour changes to a shared, carefully-reviewed + kernel-adjacent function (Tasks 4-6's own multi-round review history), + out of this task's own safe scope. Recorded here, not left silently + failing, because the orchestrating instruction's own condition for + "leave it failing" is "if you cannot explain a divergence" -- this one + IS explained, precisely; what remains unresolved is which SAFE fix to + build, not what the mechanism is. Gated on the two literal colitur + slug-family prefixes (never a bare "any citation diff" rule) so an + unrelated future bug on any OTHER slug still surfaces as unexplained. + Derived directly from the OCaml comparator's own failure output + (test_layer_c_counts_match_citations), not hand-counted first and + cross-checked after: 107. NOTE: an earlier version of this comment (and + of [colitur_keys] in tools/bootstrap_lectionary.ml) measured 202 here, + before a regression in the FIRST version of the "ef-christmas-sunday-0" + translation (replaced instead of widened the key -- caught by + test_lectionary_ef.ml's own pre-existing test, not by this reasoning) + was fixed; fixing it also legitimately shrank this population by 95 + rows. data/ef/expected-divergences.sexp's own C21/C6 notes have the full + before/after account, including the ONE guess this task tried for + [ef-nativity-octave-day-{5,6,7}] (Holy Name Sunday's own citation, + uniformly) and reverted once measured -- Missal-confirmed + (docs/research/scan1.txt, "Dominica infra octavam Nativitatis Domini, + II classis": Gal. 4,1-7 / Luc. 2,33-40) to be right for the genuine + Sunday-within-the-Octave case alone, wrong whenever the true preceding + Sunday is Christmas Day or Advent IV instead. *) +let christmastide_fixed_slug_family (c : row) = + starts_with ~prefix:"ef-nativity-octave-day-" c.slug || starts_with ~prefix:"ef-christmas-" c.slug + (* [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). *) @@ -638,7 +799,13 @@ let layer_c_reason (l : row) (c : row) diffs = then Some "C8" else if List.mem l.date joseph_annunciation_collision_dates_2005_2050 - && subset diffs [ Slug_f; Rank ] + (* Task 8: [First_f; Gospel_f] joined the accepted set -- Joseph's own + transferred office (Ecclus 45:1-6/Matt 1:18-21, his own proper) now + genuinely differs from the plain feria lectio shows for the same + civil date, exactly BECAUSE the identity already differs (this + predicate's own pre-existing [Slug_f; Rank] pair); not a second, + independent citation-only divergence. *) + && subset diffs [ Slug_f; Rank; First_f; Gospel_f ] (* Colitur-slug guard, added by the fixture-refresh review. Fix round 1's own finding 1 established that a Layer C predicate must pin WHICH celebration the citation is about, not merely the date -- C1, C6 and C8 @@ -652,16 +819,46 @@ let layer_c_reason (l : row) (c : row) diffs = then Some "C14" else if List.mem l.date holy_family_baptism_collision_dates_2005_2050 - && subset diffs [ Season; Slug_f ] + (* Task 8: [First_f; Gospel_f] joined the accepted set for the same + reason as C14 above -- Holy Family's own citations (Col 3:12-17/Luke + 2:42-52, its own proper) differ from the fixed Baptism's (which + lectio still shows there) exactly BECAUSE the office identity + already differs; the same single cause, not two. *) + && subset diffs [ Season; Slug_f; First_f; Gospel_f ] && String.equal c.slug "ef-time-after-epiphany-sunday-1" then Some "C15" else if List.mem l.date holy_name_fallback_dates_2005_2050 - && subset diffs [ Slug_f; Rank ] + (* Task 8: [First_f; Gospel_f] joined the accepted set. Now that + [ef-holy-name] carries its own citations (tools/bootstrap_lectionary.ml's + own [derived_entries], sourced from RG 17(a)'s single Mass heading + covering both shapes), most of these 20 rows already match lectio's + own citation by coincidence -- lectio's OWN plain-ferial fallback, + on years with no Sunday 2-5 January, usually lands on a December + Sunday whose slug collapses to the SAME "ef-christmas-sunday-0" key + this generator translated -- but on the 7 years where 1 January + ITSELF is a Sunday, lectio's own fallback instead finds the + Circumcision's own real Mass (Titus 2:11-15/Luke 2:21), a genuine + divergence from colitur's RG 17(a) office, which reads the SAME + Mass on 2 January regardless of what happened the day before. Both + shapes are the identical underlying cause (colitur builds RG 17(a); + lectio has no equivalent office at all), so both belong to this one + id, not a split entry. *) + && subset diffs [ Slug_f; Rank; First_f; Gospel_f ] && String.equal c.slug "ef-holy-name" then Some "C16" else if is_bvm_saturday_row c diffs then Some "C17" else if diffs = [ Colour_f ] && List.mem c.slug audit_colour_corrected_slugs then Some "C18" + else if + List.mem (year_of_date l.date) epiphany_week_shift_years_2005_2050 + && starts_with ~prefix:"ef-time-after-epiphany-" c.slug + && subset diffs [ Colour_f; First_f; Gospel_f ] + then Some "C19" + else if + diffs = [ First_f; Gospel_f ] && String.equal l.first "-" && String.equal l.gospel "-" + && List.mem c.slug lectio_no_reading_slugs + then Some "C20" + else if christmastide_fixed_slug_family c && subset diffs [ Slug_f; First_f; Gospel_f ] then Some "C21" else None (* ---------------------------------------------------------------------- *) @@ -710,10 +907,17 @@ let classify lectio colitur = | None -> (l, c, Unexplained diffs)) lectio colitur +(* Task 8: [first]/[gospel] joined the row dump on both sides -- naming the + date, the slug and the PART (via [diffs]' own field name, "first" or + "gospel") and each side's reference is exactly what the task brief asks + a citation mismatch to report, never a bare count. *) let describe_unexplained (l : row) (c : row) diffs = - Printf.sprintf "%s: %s differ -- lectio=(%s %s %s %s %s) colitur=(%s %s %s %s %s)" l.date + Printf.sprintf + "%s: %s differ -- lectio=(%s %s %s %s %s first=%s gospel=%s) colitur=(%s %s %s %s %s first=%s gospel=%s)" + l.date (String.concat "," (List.map field_name diffs)) - l.weekday l.season l.slug l.rank l.colour c.weekday c.season c.slug c.rank c.colour + l.weekday l.season l.slug l.rank l.colour l.first l.gospel c.weekday c.season c.slug c.rank c.colour + c.first c.gospel (* Fix round 1, finding 4: the fixture's own byte content must still match the SHA-256 its provenance note claims (lectio commit 2386a45, recorded |
