diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 10:04:21 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 10:04:21 +0200 |
| commit | 59fbda87d565ad9b98c2493e9703390cd988609d (patch) | |
| tree | 71ebfb8e9568cd5003c10dabc8804fba4ca66e76 /test/test_oracle.ml | |
| parent | 254cfd7d9111b0f23f9c44e82d3e31d5817dc5a4 (diff) | |
| parent | e30a4be5a2594fb3b6c4b51345d138cd89ba78ad (diff) | |
| download | colitur-59fbda87d565ad9b98c2493e9703390cd988609d.tar.gz colitur-59fbda87d565ad9b98c2493e9703390cd988609d.zip | |
Merge branch 'ef-rg112-rg110': the Holy Family, RG 112(a), observed identity
Builds the Feast of the Holy Family (RG 17(b), first Sunday after
Epiphany, II class), which did not exist in colitur at all -- it emitted
an ordinary Sunday on all 8417 days the feast occurs. Adds RG 112(a)'s
exclusion, on RG 95 paragraph 2's authority ('Si vero duo festa eiusdem
Divinae Personae ... fit de festo, quod in tabella praecedentiae
superiorem obtinet locum, et aliud omittitur'), corroborated by RG
112(a) and by the Holy Family Mass propers' own 13-January rubric.
Three validation layers had missed the missing feast, each for its own
reason: the differential because lectio lacks it too and shares
colitur's data lineage; the golden layer for want of a pin; and the
oracle because it compared the observed day's rank and colour but never
its identity. That last gap is the more important half of this change
and is now closed -- the oracle compares observed identity, with the
unresolvable population counted and pinned rather than silently skipped.
Also rescales RG 91's band table x10. entry_14_movable_band had been a
negative sentinel, chosen to avoid collision with a real table position;
it also inverted ordering, making a movable II-class feast of the Lord
outrank every I-class day. Unreachable on universal data, live the
moment a diocesan overlay places a I-class proper or indult feast in the
7-13 January window. A half-row is now an ordinary position between its
neighbours. The rescale was verified entry-by-entry against a
photographic scan and moves zero days across the domain.
RG 110 is NOT built despite the branch name; it remains open, as do the
Holy Name of Jesus office (missing in 3619 of 8417 years) and RG 98's
transfer-ordering mechanism.
Diffstat (limited to 'test/test_oracle.ml')
| -rw-r--r-- | test/test_oracle.ml | 167 |
1 files changed, 154 insertions, 13 deletions
diff --git a/test/test_oracle.ml b/test/test_oracle.ml index a86fd76..35d4501 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -3,6 +3,20 @@ layer 4 of the design spec's five (colitur CLAUDE.md "Validation" section; layer 3, the lectio differential, is test_differential.ml, already green). + *** WIP, 2026-08-13, branch ef-rg112-rg110: this file is EXPECTED RED + right now. [Observed_identity_mismatch]/[Observed_identity_unresolved] + (below) are new -- this layer used to compare the observed day's rank and + colour and stop there, never whether it is actually the RIGHT day (Holy + Family, 11 January 2026, is rank 2/white on both sides purely by + coincidence -- an ordinary unnamed Sunday and Holy Family share both). + Landing the strengthened comparator FIRST, red, before touching any + production code, is the point: it is the regression net the rest of this + task's fix commits are checked against, not a change bundled in alongside + the fix. The very next commits make it green again (data/ef/expected- + divergences-missalemeum.sexp's own M-series entries below, and this + file's own [layer_m_reason] widening) -- see that file and + docs/research/rules-register.md for the running account. + *** WHY THIS LAYER EXISTS, AND WHAT IT CAN DO THAT THE LECTIO DIFFERENTIAL CANNOT (test_differential.ml's own header comment, limit 1) *** @@ -280,6 +294,15 @@ type colitur_row = { c_date : string; c_rank : int; c_colour : char; + c_observed_slug : string; + c_observed_name : string option; + (** colitur's own resolved English name for the OBSERVED celebration -- + [None] for a TEMPORAL-origin day (an ordinary Sunday, a feria, a + movable named feast: {!Rite_ef.Temporal_ef}'s own [build] never + sets [names], the SAME gap this file's header already documents + for commemorations, see [Comm_identity_unresolved]), the same + [Names.find ... en] read [c_commemorations] below already uses, + applied to [LD.observed] instead of a commemoration candidate. *) c_commemorations : (string * int * Cel.status * string option) list; (** slug, rank, status, English name *) } @@ -321,6 +344,8 @@ let colitur_rows_2026_2027 () = { 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 } :: !rows @@ -335,7 +360,15 @@ let colitur_rows_2026_2027 () = (* header for the mapping and its exact limits. *) (* ---------------------------------------------------------------------- *) -type field = Rank | Colour_f | Comm_presence | Comm_count | Comm_identity_mismatch | Comm_identity_unresolved +type field = + | Rank + | Colour_f + | Comm_presence + | Comm_count + | Comm_identity_mismatch + | Comm_identity_unresolved + | Observed_identity_mismatch + | Observed_identity_unresolved let field_name = function | Rank -> "rank" @@ -344,6 +377,40 @@ let field_name = function | Comm_count -> "commemoration-count" | Comm_identity_mismatch -> "commemoration-identity-mismatch" | Comm_identity_unresolved -> "commemoration-identity-unresolved" + | Observed_identity_mismatch -> "observed-identity-mismatch" + | Observed_identity_unresolved -> "observed-identity-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 + celebration's rank and colour ([Rank]/[Colour_f]) and the admitted + COMMEMORATIONS' identity ([Comm_identity_*]) -- nothing until now compared + the OBSERVED celebration's own identity, WHICH day is actually being kept, + only what class/colour it happens to carry. Holy Family (11 January 2026: + oracle title "The Holy Family: Jesus, Mary & Joseph", colitur's own + observed slug the plain "ef-time-after-epiphany-sunday-1", BOTH rank 2 and + colour white on both sides) is exactly the shape this axis exists to + catch -- rank and colour already agreed by coincidence (an ordinary, + unnamed II-class Sunday and Holy Family share both), so nothing above + ever saw a difference. Same mapping and the same honesty about its limit + as [identity_diff] just above: colitur identifies a day by SLUG, + missalemeum by an English TITLE string, and the bridge is + {!Celebration.t}.names, read directly off [LD.observed] by + [colitur_rows_2026_2027] into [c_observed_name] -- resolvable for a + SANCTORAL-origin observed day (a saint's feast winning the day outright), + NEVER for a TEMPORAL-origin one (an ordinary Sunday, a feria, a movable + named feast -- {!Rite_ef.Temporal_ef}'s own [build] never sets [names]). + [Observed_identity_unresolved] is that TEMPORAL-origin case: counted and + reported, never silently skipped, the same discipline + [Comm_identity_unresolved] already established and the brief demands + again here ("a day whose observed identity cannot be resolved must be a + counted, allow-listed outcome, never a silent skip"). Unlike commemoration + identity, this axis needs no presence/count gate first -- there is always + exactly one observed day on each side, no list-length ambiguity to + resolve before a title comparison is even meaningful. *) +let observed_identity_diff (c : colitur_row) (o : oracle_row) = + match c.c_observed_name with + | None -> Some Observed_identity_unresolved + | Some name -> if String.equal name o.o_title then None else Some Observed_identity_mismatch (* Identity (Task B): only even attempted once presence AND count already agree (both non-empty, same length) -- see this file's own header, @@ -378,7 +445,8 @@ let diff_fields (c : colitur_row) (o : oracle_row) = (if c_has && o_has && List.length c.c_commemorations <> List.length o.o_commemorations then Some Comm_count else None); - identity_diff c o + identity_diff c o; + observed_identity_diff c o ] (* ---------------------------------------------------------------------- *) @@ -658,25 +726,98 @@ let m16_dates = [ "2026-03-27" ] entry needed at all -- removed, not re-adjudicated to a different verdict, since there is no longer a divergence to name. *) +(* M18 -- ef-rg112-rg110 task: the gap this file's own header now names under + "COMMEMORATION IDENTITY", restated for the OBSERVED axis. colitur's own + observed celebration carries an English name ({!Celebration.t}.names) + ONLY for a SANCTORAL-origin day (a saint's feast winning outright); a + TEMPORAL-origin one -- an ordinary Sunday, a feria, a movable named feast + including Holy Family itself -- never does ({!Rite_ef.Temporal_ef}'s own + [build] has no [en "..."] string anywhere in it, this file's own header + again). CORRECTED, fix round 1 (coordinator finding 8) -- the precise + breakdown: of this window's 730 days, 331 are RESOLVED (330 matching, 1 + mismatched, [M13]); 399 are UNRESOLVED, split between this entry's own + 373 (the axis's sole disagreement) and 26 absorbed inside [M1]/[M2]/[M3]/ + [M16]'s own widened subsets, where it fires ALONGSIDE their pre-existing + citation. The BLIND SPOT this axis exists to close, stated precisely: a + TEMPORAL-origin observed day silently replaced by a DIFFERENT + temporal-origin observed day of the SAME RANK AND COLOUR -- exactly the + shape that hid Holy Family from every layer before this task. This is + the OVERWHELMING majority of days in any calendar (saints' feasts are + common, but far from every day), so this is a single STRUCTURAL + predicate -- [Observed_identity_unresolved] alone, nothing else + disagreeing -- not a literal date list the way every other entry in this + file is: at this population size, a list would be exactly the + "range/pattern that could silently widen" this file's own header + explicitly avoids elsewhere, for the OPPOSITE reason a range is normally + risky here -- the predicate itself (colitur's own name is [None]) is the + precise, falsifiable evidence, the same shape [M2]'s own title-substring + predicate already uses instead of a date list, just keyed on + presence-of-a-name rather than a title string. NOT a rubric dispute and + NOT a data gap either engine is wrong about -- a LIMIT of this comparator + itself, honestly counted rather than silently passed, per the brief's own + explicit instruction ("a day whose observed identity cannot be resolved + must be a counted, allow-listed outcome, never a silent skip"), the SAME + discipline [M15] already established for an unresolvable COMMEMORATION, + applied here for the first time to the OBSERVED day. The count pin below + ([test_layer_m_counts_match_citations]) is what stands guard against this + population growing (or shrinking) silently -- it cannot say WHICH day + moved or why, only that the total did. Fixing this at the root (giving + every temporal-cycle candidate an English name) is a data/lectionary- + bootstrap task (Plan 4), not something this comparator can do for itself + -- register ยง6 tracks it, the same open item + [M15]'s own note already points to. *) 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" - else if contains_substring o.o_title ~needle:bvm_saturday_title && diffs = [ Colour_f ] then Some "M2" - else if List.mem c.c_date m3_dates && diffs = [ Colour_f ] then Some "M3" + (* M1's own subset widened (this task): colitur's observed day on both + dates is TEMPORAL-origin (the Sunday itself, RG33's own point -- the + 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. *) + else if List.mem c.c_date m1_dates + && subset diffs [ Rank; Colour_f; Comm_presence; Observed_identity_unresolved ] + then Some "M1" + (* M2's own subset widened (this task): every BVM-Saturday date's observed + celebration is temporal-origin (the ordinary Saturday feria colitur + computes, not the distinct Office of the BVM missalemeum's title + names -- M2's own gap), so [Observed_identity_unresolved] now fires + alongside [Colour_f] where it used to fire alone, AND alone on its own + on every Paschaltide occurrence M2's own comment already says never + reached this predicate before (colour already agrees there; only the + new axis has anything left to say). *) + else if contains_substring o.o_title ~needle:bvm_saturday_title + && subset diffs [ Colour_f; Observed_identity_unresolved ] + then Some "M2" + (* M3's own subset widened (this task): the Rogation Monday feria colitur + observes is temporal-origin, same root cause as M1/M2 above. *) + else if List.mem c.c_date m3_dates && subset diffs [ Colour_f; Observed_identity_unresolved ] then + Some "M3" else if List.mem c.c_date m5_dates && diffs = [ Comm_presence ] then Some "M5" else if List.mem c.c_date m8_dates && diffs = [ Comm_presence ] then Some "M8" 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" - (* 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" + (* M13's own subset widened AGAIN (this task, following the SAME pattern + Task B's own widening comment below records): colitur's observed + celebration here is Joseph, SANCTORAL-origin, so its name IS + resolvable -- and disagrees with missalemeum's own title outright + ([Observed_identity_mismatch], not [_unresolved]), the SAME underlying + 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. *) + else if List.mem c.c_date m13_dates + && subset diffs [ Rank; Colour_f; Comm_identity_unresolved; Observed_identity_mismatch ] + 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" + (* M16's own subset widened (this task): colitur's observed celebration + here is the temporal Passiontide feria (John Damascene is only a + COMMEMORATION on this Feast-status-losing day -- band picks the + temporal candidate outright at Class3 rank, register's own account of + this date), so [Observed_identity_unresolved] now fires alongside the + pre-existing [Comm_identity_mismatch]. *) + else if List.mem c.c_date m16_dates + && subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ] + then Some "M16" + else if diffs = [ Observed_identity_unresolved ] then Some "M18" else None (* ---------------------------------------------------------------------- *) |
