diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 17:13:06 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 17:13:06 +0200 |
| commit | a2654a1e4d8987a251bcefbad2b0f0b867a6c6e6 (patch) | |
| tree | 39360eccd3fa8ee5379fc46276d1e0ef0e6ff2b5 /test/test_oracle.ml | |
| parent | e19adb9c7ea369cafe76e7eff50e9248a4a954d0 (diff) | |
| parent | e5b368dec8fdc9ab983ee0f1dee69c37883cbe12 (diff) | |
| download | colitur-a2654a1e4d8987a251bcefbad2b0f0b867a6c6e6.tar.gz colitur-a2654a1e4d8987a251bcefbad2b0f0b867a6c6e6.zip | |
Merge branch 'ef-rg16a': RG 16(a), RG 111(b), RG 113, and commemoration identity
Closes the largest known-wrong-output item on record (RG 16(a): a Feast
of the Lord occurring on a II-class Sunday leaves the Sunday
uncommemorated -- 5996 wrong days over 1583-9999) together with RG
111(b)'s rank floor on the day it holds, and gives the oracle layer the
ability to compare commemoration identity rather than only presence and
count.
Also corrects a methodological defect that had produced wrong rulings in
both this project and its sibling: docs/research/ holds an electronic
transcription alongside two photographic scans, and the transcription
silently drops commemoration lines -- seven across the year. Reasoning
from its silence had convicted the oracle wrongly on 14 August and
ruled a genuine 9 August commemoration spurious. The scans are the
primary source; the rule is now recorded at the top of the register.
RG 113 replaces an alphabetical same-rank tie-break that had no
rubrical warrant. A bare Commemoratio has no row in RG 91's table at
all, so band no longer hands one the entry of a III-class universal
feast: 4451 days, all four reordered pairs verified against a
photographic scan, zero observed-day changes.
Diffstat (limited to 'test/test_oracle.ml')
| -rw-r--r-- | test/test_oracle.ml | 461 |
1 files changed, 416 insertions, 45 deletions
diff --git a/test/test_oracle.ml b/test/test_oracle.ml index ac8d95d..2624664 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -14,11 +14,10 @@ that file's own limit 1). missalemeum is INDEPENDENT of colitur's bootstrap chain, and its JSON carries a real admitted-commemorations array (its own RG 111-shaped selection, not lectio's rejects) -- so this - file compares THREE axes the lectio differential cannot touch at all: - rank, colour (as SET membership, see below), and commemoration presence/ - count. Slug IDENTITY is deliberately NOT compared (the brief: needs a - title->slug mapping, which is the data audit's business -- see the task - report -- not this automated comparator's). + file compares axes the lectio differential cannot touch at all: rank, + colour (as SET membership, see below), commemoration presence, count, + and (CORRECTED 2026-08-12, Task B, branch ef-rg16a -- see below) IDENTITY + -- WHICH commemoration is admitted, not only how many. *** THE FIXTURE'S OWN SHAPE (fixtures/missalemeum-ef-2026-2027.txt, fixtures/missalemeum-ef-2026-2027.provenance, tools/ @@ -29,7 +28,59 @@ between the id-embedded rank and info.rank itself (task brief's own premise was wrong here; the brief's extraction note has been corrected). This file, like the extraction script, reads ONLY info.rank and - info.colors, never parses anything out of info.id. + info.colors for the DAY's own rank/colour, never parses anything out of + info.id for that purpose. + + *** COMMEMORATION IDENTITY (added 2026-08-12, Task B, branch ef-rg16a) + -- CORRECTING the previous header's own "deliberately NOT compared" + claim *** + + The gap this correction closes: layer 4 used to compare commemoration + PRESENCE and List.length ONLY -- cardinality, never identity. A reviewer + proved this was vacuous by reversing admit's own dignity/precedence sort + (Rite_ef.Precedence_ef, so the engine admits the WORST candidate, an + outright RG 111 violation) and finding every assertion in this file + still green across all 730 days (see the perturbation record in the task + report, reproduced and reverted, not left in the tree). This file now + also compares WHICH commemoration is admitted, using two independent + signals from missalemeum's own JSON: the commemoration's title text + (English, exact string) and, for a sancti-origin commemoration, its own + [id]'s embedded rank digit (used only as a corroborating check, never as + the primary signal -- see [Comm_identity] below). + + THE MAPPING, AND EXACTLY WHAT IT CANNOT RESOLVE: missalemeum identifies + a commemoration by an English TITLE string; colitur identifies one by a + SLUG. There is no hand-built slug<->title lookup table here -- colitur's + own {!Celebration.t}.names already carries an English name for every + SANCTORAL-origin celebration (bootstrapped from lectio's own English + titles, which verifiably match missalemeum's titles character-for- + character on every checked row), so the comparison reads that name + directly off colitur's own resolved output, the same field + test_differential.ml already treats as authoritative for OF/EF display + text. This resolves identity for every SANCTORAL-origin commemoration. + It CANNOT resolve a TEMPORAL-origin commemoration (an impeded feria, a + privileged Sunday, an Ember/Rogation day, etc.): {!Rite_ef.Temporal_ef}'s + own [build] never sets a celebration's [names] field (rite_ef/ + temporal_ef.ml has no [en "..."] string anywhere in it), so every such + candidate's own title is empty on colitur's side -- there is nothing to + compare against missalemeum's title text, and no principled way to + invent one without duplicating missalemeum's own English-prose + convention ("Feria III after the I Sunday of Lent") as a second, + unverified source of truth. THIS CASE IS NEVER SILENTLY SKIPPED: see + [Comm_identity_unresolved] below -- it is counted, reported, and must be + named in the allow-list like any other difference, exactly the discipline + the brief demands ("a day whose commemoration cannot be matched must be + visibly counted and reported, not quietly passed"). + + Identity is only even ATTEMPTED once presence and count already agree + (both non-empty, same length) -- a cardinality mismatch is already + [Comm_presence]/[Comm_count]'s own business (see [diff_fields] below), + and comparing SETS of different size would just restate that mismatch + under a third name, not add information. Compared as a SORTED MULTISET + of titles, not a sequence: missalemeum's own [commemorations] array order + is not independently verified against RG 113's own "commemoratio de + Tempore fit primo loco" ordering rule here, so this axis proves WHICH + commemorations are admitted, not what order they are listed in. info.colors is an ARRAY -- 14 of 730 days carry two colours (rose+violet on Gaudete/Laetare per RG 131's indult, red+violet on Palm Sunday, black+ @@ -56,14 +107,26 @@ entries; RG 110's inseparable-Peter/Paul commemoration is unimplemented code, a real feature this task did not build) -- honestly verdicted [missalemeum] (colitur is short a feature or a row, not right), never - silently absorbed as if colitur were correct. TWO entries (M11 and - M13) are [verdict open] -- CORRECTED, final fix wave, item 7: this - comment previously said "one entry (M13)", missing M11, whose own - verdict was changed from [colitur] to [open] in fix round 1 (see M11's - own entry below for why) but this summary was never updated to match. - Both are adjudicated as unresolved, not resolved either way -- the - brief's own explicit permission ("say so as an open item") used for - real, not defaulted past. See the task report for every entry's full + silently absorbed as if colitur were correct. TWO entries (M11 and M13) + are [verdict open] -- CORRECTED, final fix wave, item 7: this comment + previously said "one entry (M13)", missing M11, whose own verdict was + changed from [colitur] to [open] in fix round 1 (see M11's own entry + below for why) but this summary was never updated to match. Task B + (branch ef-rg16a) briefly added a THIRD, M17 (the same-band tie-break + between Maurice and Thomas of Villanova, 22 September) -- CORRECTED, + fix round 1 of that same task: M17 was itself wrong. The "tie" was + manufactured by {!Precedence_ef.band} lending a [Commemoration_only] + candidate the same table entry as a genuine [Feast] of its own rank + (RG 91's table has no row for a bare commemoration at all -- fixed in + [band] itself, not here); once fixed, 22 September resolves cleanly on + both sides and M17 was deleted, not merely re-adjudicated. ONE entry + (M15) carries its own fourth verdict, [unresolvable] -- not a rubric + dispute or a data gap either engine is wrong about, but a LIMIT of this + comparator itself (see M15's own entry). All remaining OPEN entries are + adjudicated as unresolved/unresolvable, not resolved either way -- the + brief's own explicit permission ("say so as an open item") used for real, + not + defaulted past. See the task report for every entry's full reasoning and primary-source citation. *) module Cal = Colitur_kernel.Calendar @@ -74,6 +137,8 @@ module Slug = Colitur_kernel.Slug module Date = Colitur_kernel.Date module Cel = Colitur_kernel.Celebration module Colour = Colitur_kernel.Colour +module Names = Colitur_kernel.Names +module Lang = Colitur_kernel.Lang module V = Rite_ef.Vocab_ef (* Same relative paths test_differential.ml uses: dune test runs from @@ -88,7 +153,16 @@ let allow_list_path = "../data/ef/expected-divergences-missalemeum.sexp" same reason test_differential.ml's own [fixture_sha256] is: a hand-edit or partial re-extraction would otherwise silently turn the oracle into an unlabelled snapshot of whatever someone last ran. *) -let fixture_sha256 = "718eff1a03fa09c3927ef18d19fe7921c2d4d7611890c22566058b81fd7cc5ea" +(* CORRECTED 2026-08-12, Task B (branch ef-rg16a): field 9 + (commemoration_ids) appended to the fixture (tools/ + extract_missalemeum_oracle.py's own header) so this file can compare + commemoration IDENTITY, not merely presence/count -- see this file's own + header comment, rewritten below. Regenerated from the SAME underlying + 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" (* Identical technique to test_differential.ml's own [sha256_of_file] (that file's own comment explains why: shelling out to [sha256sum] @@ -143,6 +217,7 @@ type oracle_row = { o_colours : char list; o_title : string; o_commemorations : string list; + o_commemoration_ids : string list; (** parallel to [o_commemorations], see field 9's own doc above *) o_displaced : string list; } @@ -150,16 +225,27 @@ let explode s = List.init (String.length s) (String.get s) let split_list_field f = if f = "-" then [] else String.split_on_char ';' f +(* A commemoration id's own trailing "rank" field, e.g. "sancti:01-05:4:r" -> + [Some 4] -- the SECOND, corroborating identity signal this file's own + header describes (test_identity_rank_corroboration, near the bottom of + this file). [None] for any id that does not have exactly 4 ':'-separated + parts (defensive; every id checked so far does). *) +let id_rank id = + match String.split_on_char ':' id with [ _; _; rank; _ ] -> int_of_string_opt rank | _ -> None + +let is_sancti_id id = String.length id >= 7 && String.sub id 0 7 = "sancti:" + let oracle_row_of_line line = match String.split_on_char '|' line with - | [ date; rank; colours; title; _tempora; commemorations; displaced; _n_masses ] -> + | [ date; rank; colours; title; _tempora; commemorations; displaced; _n_masses; commemoration_ids ] -> { 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 8 '|'-separated fields): %S" line + | _ -> Alcotest.failf "malformed fixture line (expected 9 '|'-separated fields): %S" line let read_lines path = let ic = open_in path in @@ -183,7 +269,20 @@ let oracle_rows () = List.map oracle_row_of_line (read_lines fixture_path) (* year, which straddles two civil years). *) (* ---------------------------------------------------------------------- *) -type colitur_row = { c_date : string; c_rank : int; c_colour : char; c_commemorations : string list } +(* [c_commemorations]: one entry per admitted commemoration, carrying its + own slug, RG 8 rank, STATUS, AND its English name if colitur's own + {!Celebration.t}.names has one -- [None] for a TEMPORAL-origin + candidate, which [Rite_ef.Temporal_ef] never names (this file's own + header, "THE MAPPING, AND EXACTLY WHAT IT CANNOT RESOLVE"). The [None] + case is what [Comm_identity_unresolved] below reads; [status] is read by + [test_identity_rank_corroboration]'s own scope guard, see its comment. *) +type colitur_row = { + c_date : string; + c_rank : int; + c_colour : char; + c_commemorations : (string * int * Cel.status * string option) list; + (** slug, rank, status, English name *) +} let rank_to_int = function V.Class1 -> 1 | V.Class2 -> 2 | V.Class3 -> 3 | V.Class4 -> 4 @@ -195,6 +294,8 @@ let colour_to_char = function | Colour.Rose -> 'p' | Colour.Black -> 'b' +let en = Lang.of_string_exn "en" + let colitur_rows_2026_2027 () = let layer = real_layer () in let by_rata : (int, (V.season, V.rank) LD.t) Hashtbl.t = Hashtbl.create 800 in @@ -210,14 +311,17 @@ let colitur_rows_2026_2027 () = (match Hashtbl.find_opt by_rata (Date.to_rata !d) with | Some day -> let cel = day.LD.observed in - let commemoration_slugs = - List.map (fun (c, _) -> Slug.to_string c.Cel.slug) day.LD.commemorations + let commemorations = + List.map + (fun (c, _) -> + (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 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_commemorations = commemoration_slugs + c_commemorations = commemorations } :: !rows | None -> Alcotest.failf "internal error: no resolved day for %s" (Date.to_iso8601 !d)); @@ -226,18 +330,39 @@ let colitur_rows_2026_2027 () = List.rev !rows (* ---------------------------------------------------------------------- *) -(* Field-diff computation: the four axes the oracle actually supports *) -(* (identity-by-slug is deliberately out of scope, see this file's own *) -(* header). *) +(* Field-diff computation: the axes the oracle supports, RANK/COLOUR/ *) +(* PRESENCE/COUNT as before, plus (Task B) IDENTITY -- see this file's own *) +(* header for the mapping and its exact limits. *) (* ---------------------------------------------------------------------- *) -type field = Rank | Colour_f | Comm_presence | Comm_count +type field = Rank | Colour_f | Comm_presence | Comm_count | Comm_identity_mismatch | Comm_identity_unresolved let field_name = function | Rank -> "rank" | Colour_f -> "colour" | Comm_presence -> "commemoration-presence" | Comm_count -> "commemoration-count" + | Comm_identity_mismatch -> "commemoration-identity-mismatch" + | Comm_identity_unresolved -> "commemoration-identity-unresolved" + +(* Identity (Task B): only even attempted once presence AND count already + agree (both non-empty, same length) -- see this file's own header, + "Identity is only even ATTEMPTED...". Two outcomes beyond a clean match: + [Comm_identity_unresolved] when ANY of colitur's own admitted + commemorations carries no English name to compare (a TEMPORAL-origin + candidate -- counted and reported, never silently skipped, per the + brief); [Comm_identity_mismatch] when every name IS resolvable but the + SORTED MULTISET of titles still disagrees with missalemeum's own. *) +let identity_diff (c : colitur_row) (o : oracle_row) = + let c_has = c.c_commemorations <> [] and o_has = o.o_commemorations <> [] in + if not (c_has && o_has) || List.length c.c_commemorations <> List.length o.o_commemorations then None + else + let unresolved = List.exists (fun (_, _, _, name) -> name = None) c.c_commemorations in + if unresolved then Some Comm_identity_unresolved + else + let c_titles = List.sort compare (List.filter_map (fun (_, _, _, n) -> n) c.c_commemorations) in + let o_titles = List.sort compare o.o_commemorations in + if c_titles = o_titles then None else Some Comm_identity_mismatch let diff_fields (c : colitur_row) (o : oracle_row) = let c_has = c.c_commemorations <> [] and o_has = o.o_commemorations <> [] in @@ -252,7 +377,8 @@ let diff_fields (c : colitur_row) (o : oracle_row) = same disagreement under a second name. *) (if c_has && o_has && List.length c.c_commemorations <> List.length o.o_commemorations then Some Comm_count - else None) + else None); + identity_diff c o ] (* ---------------------------------------------------------------------- *) @@ -345,19 +471,31 @@ let m4_dates = [ "2026-01-28"; "2027-01-28" ] report). *) let m5_dates = [ "2026-04-25" ] -(* M6 -- the reverse of M4/M7/M9: the primary calendarium's own row for 14 - August reads, in full, "Vigilia, II classis." -- nothing else. No - commemoration is listed for that date anywhere in the source this - project treats as authoritative (CLAUDE.md binding decision 1). data/ef/ - sanctoral.sexp agrees (a single entry, [vigil-of-the-assumption], 14 - Aug). missalemeum's own "St. Eusebius" commemoration for that date has - no support in the 1962 universal calendar found here (a DIFFERENT St - Eusebius, bishop and martyr, is genuinely commemorated 16 December, - matching lectio's own [eusebius] entry at that date -- not this one). - Verdict colitur: this is the one direction among M4/M6/M7/M9 where the - oracle itself appears to be carrying data outside the 1962 universal - calendar (a pre-1955 survival or a different edition), not colitur - missing something real. *) +(* M6 -- CORRECTED 2026-08-12, transcription-audit task (branch ef-rg16a): + the previous version of this comment read the ELECTRONIC TRANSCRIPTION + (1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum,_LT.pdf), which silently + drops calendarium commemorations, and mistook its silence for the + calendarium's own -- "reads, in full, 'Vigilia, II classis.' -- nothing + else" was true of the TRANSCRIPTION only. A photographic scan + (missale-romanum-1962.pdf), 14 August row: "Vigilia, II classis, + Commemoratio S. Eusebii Conf.", with its own proper immediately + following ("Eodem die ... Et fit commemoratio S. Eusebii Conf.") -- + independently confirmed in the second scan, Missale Romanum 1962_text + .pdf. The same shape as M4/M7/M9/M14, NOT their reverse: a genuine + commemoration is present in the calendarium and in missalemeum's own + data, but absent from data/ef/sanctoral.sexp and lectio's own + tridentine-calendar.ini (no 08-14 entry beyond [vigil-of-the-assumption]). + This Eusebius Confessor (14 Aug) is a genuinely DIFFERENT saint from + Eusebius Bishop and Martyr (16 Dec, already correctly in data/ef/ + sanctoral.sexp, matching lectio's own [eusebius] entry at THAT date) -- + both exist, at their own separate dates. Verdict missalemeum (flipped + from colitur): colitur is missing this commemoration, a genuine data gap + not fixable without touching ~/git/projects/lectio -- see + docs/research/rules-register.md, Decisions item 4 and the + transcription-audit record in section 6, for the full account and the + wider calendarium sweep this correction came from. The diff SHAPE this + predicate matches (Comm_presence, both dates) is unchanged by the + verdict flip -- only which side is right, and why, changed. *) let m6_dates = [ "2026-08-14"; "2027-08-14" ] (* M7 -- a genuine DATA GAP, primary-source-confirmed, the same shape as @@ -479,6 +617,89 @@ let m12_dates = [ "2027-02-22" ] item. *) let m13_dates = [ "2027-03-19" ] +(* M15 -- Task B (branch ef-rg16a), NOT a rubric dispute and NOT a data gap + either engine is wrong about: a genuine LIMIT of this comparator, honestly + counted rather than silently passed (the brief's own explicit + instruction: "a day whose commemoration cannot be matched must be + visibly counted and reported, not quietly passed"). On each of these 19 + rows, colitur's own admitted commemoration is TEMPORAL-origin (an + impeded Advent/Lent feria) -- Rite_ef.Temporal_ef's own [build] + (temporal_ef.ml) never sets a celebration's [names] field, so there is no + English string on colitur's side to compare against missalemeum's title + text at all (this file's own header, "THE MAPPING, AND EXACTLY WHAT IT + CANNOT RESOLVE"). Every one of these 19 oracle titles reads as a plain + prose description of the SAME temporal ferial slot colitur independently + computes for that date (e.g. 2 Dec 2026: oracle "Feria IV after I Sunday + of Advent" against colitur's own [ef-advent-1-wednesday]; 4 Dec 2027: + oracle "Sabbato after I Sunday of Advent" against [ef-advent-1-saturday]) + -- suggestive corroboration, recorded here for a human reader, but NOT + proof: this comparator has no mechanism to verify English prose against + an opaque slug, so it does not claim these as verified matches, only as + unresolved. Fixing this at the ROOT (giving every temporal-cycle + candidate an English name) is a data/lectionary-bootstrap task, not + something this comparator can do for itself -- register §6, "RG 113 + tie-break" section's own neighbour, records it as an open item. *) +let m15_dates = + [ "2026-02-24"; "2026-03-19"; "2026-03-25"; "2026-12-02"; "2026-12-03"; "2026-12-04"; + "2026-12-07"; "2026-12-11"; "2026-12-21"; "2027-02-24"; "2027-12-02"; "2027-12-03"; + "2027-12-04"; "2027-12-06"; "2027-12-07"; "2027-12-11"; "2027-12-13"; "2027-12-16"; + "2027-12-21" ] + +(* M16 -- Task B: a genuine IDENTITY mismatch, both sides otherwise agreeing + on rank/colour/count/presence. 27 March 2026 (Friday of Passion Week): + colitur admits "St. John Damascene" (a real Class3 universal feast that + genuinely falls that day); missalemeum shows "For Our Lady of the Seven + Sorrows" instead. The SAME standing gap M13 above already names ("the + 1962 calendarium's own March table carries... 'Feria VI post dominicam I + Passionis: Commemoratio septem Dolorum B. Mariae Virg.'... a fixed + commemoration... EVERY year") -- confirmed real, and confirmed + UNIMPLEMENTED in this codebase (no candidate for it is ever constructed + in temporal_ef.ml), so colitur has nothing to admit in the Seven + Sorrows' place and falls back to whichever ordinary Class3 saint + actually wins that Friday instead. Verdict missalemeum: colitur is + missing a real, primary-cited office, not differently opinioned. + Register §6 open item (same one M13/M16's own primary-source note + already tracks). Only 2026 shows here -- 2027's Friday of Passion Week + IS 19 March, M13's own date, where the identity axis cannot even be + reached (M13's own rank/colour mismatch already excludes that day from + count-matched identity comparison). + + NOTE for whoever builds the office (fix round 1, coordinator finding 7): + 27 March 2026 is a III-class day, where RG 111(d) admits TWO + commemorations, not one -- yet missalemeum admits only the Seven Sorrows + and DISPLACES John Damascene entirely (its own "displaced" list carries + his title that day), not merely drops him to second place. Implementing + the Seven Sorrows candidate naively (as one more ordinary III-class + commemoration competing for the day's two slots) will not reproduce + this: John Damascene would still win one of the two admitted slots by + dignity/band, giving colitur TWO commemorations where missalemeum shows + one. Whatever privilege or precedence the Seven Sorrows commemoration + carries must itself explain the exclusion, not just the admission -- + register §6's own open item for this office should carry this caveat + forward. *) +let m16_dates = [ "2026-03-27" ] + +(* M17 was DELETED, fix round 1 (Task B): the "genuine tie" it adjudicated + as [open] was itself wrong. 22 September (any year the September Ember + Wednesday falls on the 22nd -- 2027 in this window): colitur used to + admit "St. Maurice and Companions, Martyrs" (Commemoration_only, Class3) + where missalemeum shows "St. Thomas of Villanova" (Feast, Class3) -- + NOT because RG 113 runs out of instruction between two same-rank + candidates (the framing this entry used to carry), but because + {!Precedence_ef.band} used to lend a [Commemoration_only] candidate the + SAME table entry (24) as a genuine [Feast] of its own rank, manufacturing + a tie the primary text never creates: RG 91's own table enumerates only + "dies liturgici" (entry 24: "Festa III classis..." -- FEASTS), and the + calendarium's own 22 September row confirms it in its own notation -- + "S. Thomae de Villanova Ep. et Conf., III classis. / Commemoratio Ss. + Mauritii et Soc. Mm." -- Thomas carries a class number, Maurice carries + none. Fixed at the source ([band] itself now returns [Precedence_ef + .unclassified] for any [Commemoration_only] candidate, docs/research/ + rules-register.md §6.1's own corrected account) rather than here: 22 + September now resolves identically on both sides with no allow-list + entry needed at all -- removed, not re-adjudicated to a different + verdict, since there is no longer a divergence to name. *) + let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = if diffs = [] then None else if List.mem c.c_date m1_dates && subset diffs [ Rank; Colour_f; Comm_presence ] then Some "M1" @@ -494,7 +715,15 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = else if List.mem c.c_date m10_dates && diffs = [ Comm_presence ] then Some "M10" else if List.mem c.c_date m11_dates && diffs = [ Comm_presence ] then Some "M11" else if List.mem c.c_date m12_dates && diffs = [ Comm_count ] then Some "M12" - else if List.mem c.c_date m13_dates && subset diffs [ Rank; Colour_f ] then Some "M13" + (* M13's own subset widened (Task B): the same underlying gap (the Seven + Sorrows commemoration is never constructed at all) now ALSO surfaces + as [Comm_identity_unresolved] on this date, since colitur's own + admitted commemoration there is temporal-origin (no English name) -- + one root cause, one id, both axes it touches. *) + else if List.mem c.c_date m13_dates && subset diffs [ Rank; Colour_f; Comm_identity_unresolved ] then + Some "M13" + else if List.mem c.c_date m15_dates && diffs = [ Comm_identity_unresolved ] then Some "M15" + else if List.mem c.c_date m16_dates && diffs = [ Comm_identity_mismatch ] then Some "M16" else None (* ---------------------------------------------------------------------- *) @@ -538,17 +767,24 @@ let classify oracle colitur = | None -> (c, o, Unexplained diffs)) oracle colitur +let status_to_string = function Cel.Feast -> "Feast" | Cel.Commemoration_only -> "Commemoration_only" + +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_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] displaced=[%s])" + title=%S comms=[%s] comm_ids=[%s] displaced=[%s])" c.c_date (String.concat "," (List.map field_name diffs)) c.c_rank c.c_colour - (String.concat ";" c.c_commemorations) + (String.concat ";" (List.map describe_comm c.c_commemorations)) 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) let test_fixture_checksum () = @@ -566,9 +802,11 @@ let test_dates_align () = in Alcotest.(check (list (pair string string))) "no misaligned dates" [] mismatched -(* The core assertion: every one of the 42 raw differences across all four +(* The core assertion: every one of the 63 raw differences across all six axes (rank, colour-membership, commemoration-presence, commemoration- - count) is named in the cited allow-list. Nothing else passes silently. *) + count, and, since Task B/ef-rg16a, commemoration-identity-mismatch and + commemoration-identity-unresolved) is named in the cited allow-list. + Nothing else passes silently. *) let test_no_unexplained_differences () = let oracle, colitur = compare_streams () in let classified = classify oracle colitur in @@ -604,6 +842,136 @@ let test_layer_m_counts_match_citations () = undeclared" expected actual +(* Corroboration (Task B): for every day where the identity axis is CLEAN + (no [Comm_identity_mismatch]/[Comm_identity_unresolved] -- titles matched + as a set), independently cross-check missalemeum's own id-embedded rank + digit against colitur's OWN candidate rank for that SAME title -- a + SECOND, independent signal beyond string equality (this file's own + header: "used only as a corroborating check, never as the primary + signal"). Restricted to "sancti:"-origin ids (a TEMPORAL-origin oracle + commemoration has no colitur-side name to pair it with in the first + place, see [Comm_identity_unresolved]'s own days, which never reach here + since their identity axis is not clean). + + SPLIT BY STATUS -- a genuine finding from running this check, not the + originally-planned single assertion: EVERY ONE of colitur's 114 + [Commemoration_only] sanctoral entries carries a bootstrap-assigned + [rank Class3] (verified: `grep -c "status Commemoration_only"` and + `grep -B3 ... | grep -oP "rank Class\d" | sort | uniq -c` both give + 114/114 Class3, data/ef/sanctoral.sexp) -- but missalemeum's OWN id-rank + for a [Commemoration_only] match is NOT itself uniform: of the 175 + examined, 17 genuinely AGREE (oracle_rank = colitur_rank = 3, real + corroboration) and 158 show oracle_rank 4 against colitur's bootstrapped + 3 -- CORRECTED after the first version of this comment overclaimed "id + consistently encodes 4 for every entry" from the 158-strong failure list + alone, before the 17 agreeing rows (then hidden inside a too-strict + assertion) were found. Two acceptable shapes, then, not one: either the + ranks genuinely agree, or the specific oracle=4/colitur=3 gap holds. This + is not evidence of a title-matching error, and not new: it independently + corroborates the register's own already-open item (§6, "Commemoration- + only entries' inferred STATUS, not only rank" -- Task 16's own data + audit already flagged these ranks as bootstrap inferences, never + individually Missal-verified). Auditing which of the 114 are genuinely + Class3 and which were silently defaulted to it is that item's own scope, + not this task's -- so rather than either asserting a false universal + equality (which fails on the 158 KNOWN, tracked, out-of-scope rows) or + silently dropping the check, this pins the discovered SHAPE itself: + every [Commemoration_only] match is EITHER a real agreement OR the known + gap, with no THIRD pattern -- a real, falsifiable claim (any other + combination would break it) that stays honest about what remains + unverified. + + WHAT THIS DOES NOT PIN (coordinator finding 8, fix round 1, honestly + named rather than left implicit): the [oracle_rank = colitur_rank] + branch accepts ANY genuine agreement, including one this check cannot + independently verify is the CORRECT rank -- a regression that silently + flipped some entry's [rank] from [Class3] to [Class4] would land in the + agreement branch and pass cleanly if missalemeum's own id happened to + read 4 for that entry too (data drift on one side coinciding with data + drift on the other is not ruled out by this check, only coincidence + independent of any real cause is). This test pins "no third pattern + appears", not "every individual rank is correct" -- a narrower, still + genuinely useful claim (see the [> 50] population guard below, which + confirms the pinned shape is actually exercised at scale, not vacuously + true over an empty or trivial set), and this comment says so rather than + letting the assertion's own name imply more than it checks. + + [Feast]-status candidates get the ORIGINAL, unrestricted check (any + disagreement at all is unexpected). CORRECTED (fix round 1, coordinator + finding 1): this comment previously claimed this branch was unreachable + in the 2026-2027 window (checked: 0) because its one candidate, Thomas + of Villanova on 22 September, was M17's own mismatch -- WRONG, once + traced further: M17 itself was wrong (see {!Precedence_ef.band}'s own + fidelity fix, register §6.1), and fixing it made 22 September resolve + cleanly on both sides, reachable here after all. Measured, not assumed: + 22 Feast-status commemorations are now examined by this branch (not + only Thomas of Villanova -- every OTHER genuinely clean Feast-status + match in the window reaches it too, which the earlier version of this + comment did not check for before asserting "0"). All 22 agree. *) +let test_identity_rank_corroboration () = + let oracle, colitur = compare_streams () in + let feast_checked = ref 0 in + let feast_mismatches = ref [] in + let commemoration_only_checked = ref 0 in + let commemoration_only_surprises = ref [] in + List.iter2 + (fun (o : oracle_row) (c : colitur_row) -> + if + identity_diff c o = None && c.c_commemorations <> [] && o.o_commemorations <> [] + && List.length c.c_commemorations = List.length o.o_commemorations + then + List.iter2 + (fun title id -> + if is_sancti_id id then + match id_rank id with + | None -> () + | Some oracle_rank -> ( + match List.find_opt (fun (_, _, _, n) -> n = Some title) c.c_commemorations with + | Some (slug, colitur_rank, Cel.Feast, _) -> + incr feast_checked; + if colitur_rank <> oracle_rank then + feast_mismatches := + Printf.sprintf "%s: %s oracle-id-rank=%d colitur-rank=%d" c.c_date slug + oracle_rank colitur_rank + :: !feast_mismatches + | Some (slug, colitur_rank, Cel.Commemoration_only, _) -> + incr commemoration_only_checked; + (* Two ACCEPTABLE shapes, not one -- corrected after first running this + check: most Commemoration_only entries genuinely agree + (oracle_rank = colitur_rank, real corroboration, 17 of the + population checked), and the rest show the KNOWN + oracle=4/colitur=3 convention gap (this comment's own header). + Anything OUTSIDE those two shapes (e.g. oracle=2 or a THIRD + colitur rank paired with a non-4 oracle rank) is a genuine + surprise. *) + if not (oracle_rank = colitur_rank || (oracle_rank = 4 && colitur_rank = 3)) then + commemoration_only_surprises := + Printf.sprintf "%s: %s oracle-id-rank=%d colitur-rank=%d (neither agrees nor \ + fits the known oracle=4/colitur=3 pattern)" + c.c_date slug oracle_rank colitur_rank + :: !commemoration_only_surprises + | None -> ())) + o.o_commemorations o.o_commemoration_ids) + oracle colitur; + Alcotest.(check (list string)) + "Feast-status matches: oracle id-rank agrees with colitur's own rank" [] (List.rev !feast_mismatches); + Alcotest.(check (list string)) + "Commemoration_only-status matches: every one fits the KNOWN oracle=4/colitur=3 convention gap -- any \ + other combination would be a genuine, new surprise" + [] (List.rev !commemoration_only_surprises); + (* Vacuity guard: BOTH branches must actually run now. CORRECTED (fix + round 1, coordinator finding 1): the [Feast] branch used to be + unreachable in this window (checked: 0) because its one candidate, + Thomas of Villanova on 22 September, was M17's own mismatch (excluded + by the [identity_diff = None] guard above). Fixing {!Precedence_ef + .band}'s Commemoration_only fidelity (register §6.1) made that day + resolve cleanly, so it is reachable here too -- this guard now expects + at least 1, not merely documents the branch as dormant (measured: 22 + Feast-status commemorations now examined, up from 0). *) + Alcotest.(check bool) "the Feast-status population actually examined is non-trivial" true (!feast_checked > 0); + Alcotest.(check bool) "the Commemoration_only population actually examined is non-trivial" true + (!commemoration_only_checked > 50) + let suite = ( "oracle (missalemeum, EF, 2026-2027)", [ Alcotest.test_case "fixture SHA-256 matches its provenance note" `Quick test_fixture_checksum; @@ -611,5 +979,8 @@ let suite = Alcotest.test_case "every difference is named in the cited allow-list -- none unexplained" `Quick test_no_unexplained_differences; Alcotest.test_case "allow-list counts match data/ef/expected-divergences-missalemeum.sexp exactly" - `Quick test_layer_m_counts_match_citations + `Quick test_layer_m_counts_match_citations; + Alcotest.test_case + "identity corroboration: matched titles' oracle id-rank agrees with colitur's own rank" `Quick + test_identity_rank_corroboration ] ) |
