aboutsummaryrefslogtreecommitdiff
path: root/test/test_oracle.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_oracle.ml')
-rw-r--r--test/test_oracle.ml167
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
(* ---------------------------------------------------------------------- *)