summaryrefslogtreecommitdiff
path: root/test/test_validate.ml
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-21 14:42:30 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-21 14:42:30 +0200
commit12b97761019cfa02ca0da8a5fb50ef815d07685c (patch)
tree11e7306a1a5de2f5226eea06282515ff38b226d5 /test/test_validate.ml
parent0eae32f67145065b0e4960650ae06c293fd9f272 (diff)
downloadcolitur-12b97761019cfa02ca0da8a5fb50ef815d07685c.tar.gz
colitur-12b97761019cfa02ca0da8a5fb50ef815d07685c.zip
feat(ef): implement RG 33's third omission trigger
RG 33 omits a II/III-class vigil in three cases: it falls on a Sunday, it falls on a I-class feast, "vel si festum cui praemittitur in alium diem transferri aut ad commemorationem reduci contingat". Only the first two were built; the third was recorded in precedence_ef.ml as unimplemented on the grounds that no witness existed in the shipped data. That reasoning was wrong, and the rule fires on 1 744 days across 1583-9999. Both halves of the clause reduce to one observable question -- is the feast the OBSERVED office on the following day (RG 34 puts it there) -- so the kernel asks it once per candidate, after place_transfers has settled the year. No fixed point is needed: a vigil is a candidate only on its own day, never on its feast's, so suppressing it cannot change what the next day observes. Precedence.rules gains vigil_feast, which returns the slug of the feast a vigil precedes; the kernel cannot infer that itself, because only two of the five vigil/feast pairs share a slug stem. Blast radius, measured pre-change binary vs HEAD over the whole domain and classified: 1 744 days, three shapes, zero unexplained. 1 199 are the feast reduced to a commemoration (10 August on a Sunday, St Lawrence); 478 and 67 are the feast transferred under RG 96 after the Sacred Heart or Corpus Christi takes its day. The Assumption's and the Ascension's vigils never qualify -- their I-class feasts always keep their own day. Independently witnessed, which is unusual here. The published Ordo -- the only witness outside the Divinum Officium -> missalemeum -> lectio lineage -- omits St Lawrence's vigil on 2025-08-09, agreeing with colitur against both engines. That date had been read earlier as an Ordo gap; the Ordo was right, and correcting the misreading is what surfaced this clause. On 2027-08-09 the feast does keep its day and the Ordo omits a vigil colitur correctly keeps, which is a genuine Ordo gap. Allow-lists: C39 (lectio, 10 rows) and a 2038 oracle class citing the register, the 2026-2027 window having no instance. The golden pin asserting St Lawrence's vigil is violet moved 2025 -> 2027; its own comment had reasoned about the vigil's weekday and missed that RG 33 also looks at the feast's. Two new pins cover both shapes of the clause. The vigil/feast table is built with Slug.of_string_exn: mutation testing showed that of_string plus Result.to_option turns a typo into None, which this hook's contract reads as "not a vigil", switching the rule off in silence. Two tests assert the table against the shipped data in both directions.
Diffstat (limited to 'test/test_validate.ml')
-rw-r--r--test/test_validate.ml12
1 files changed, 6 insertions, 6 deletions
diff --git a/test/test_validate.ml b/test/test_validate.ml
index 4d5458f..8b3014a 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -247,7 +247,7 @@ module Synthetic = struct
fixtures further down override [rules] (and supply a non-empty
[layer]), each with its own small, deliberately-shaped ruleset. *)
let rules : (season, rank) P.rules =
- { P.band = (fun _ _ -> 0);
+ { P.vigil_feast = (fun _ -> None); band = (fun _ _ -> 0);
disposition = (fun ~winner:_ ~loser:_ -> P.Omit);
admit = (fun ~observed:_ ~temporal:_ _ -> []) }
@@ -425,7 +425,7 @@ module Synthetic = struct
let dup_layer = Layer.of_entries ~id:"dup" ~name:"dup" [ dup_entry ]
let dup_rules : (season, rank) P.rules =
- { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10);
+ { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10);
disposition = (fun ~winner:_ ~loser:_ -> P.Commemorate P.Ordinary);
admit =
(fun ~observed:_ ~temporal:_ cs ->
@@ -464,7 +464,7 @@ module Synthetic = struct
let guard_layer = Layer.of_entries ~id:"guard" ~name:"guard" [ guard_winner_entry; guard_loser_entry ]
let guard_rules : (season, rank) P.rules =
- { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 50 | P.Sanctoral -> 10);
+ { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 50 | P.Sanctoral -> 10);
disposition =
(fun ~winner:_ ~loser ->
match loser.P.cel.Cel.rank with R1 -> P.Transfer | R2 -> P.Commemorate P.Ordinary);
@@ -493,7 +493,7 @@ module Synthetic = struct
| x :: xs -> if n <= 0 then [] else x :: adm_take (n - 1) xs
let adm_rules : (season, rank) P.rules =
- { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10);
+ { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10);
disposition = (fun ~winner:_ ~loser:_ -> P.Commemorate P.Ordinary);
admit =
(fun ~observed:_ ~temporal:_ cs ->
@@ -520,7 +520,7 @@ module Synthetic = struct
let collide_layer = Layer.of_entries ~id:"collide" ~name:"collide" [ collide_a_entry; collide_b_entry ]
let collide_rules : (season, rank) P.rules =
- { P.band =
+ { P.vigil_feast = (fun _ -> None); band =
(fun ctx c ->
match c.P.origin with
| P.Temporal -> if D.compare ctx.P.date collide_d1 = 0 then 5 else 50
@@ -543,7 +543,7 @@ module Synthetic = struct
let clean_sanctoral_layer = Layer.of_entries ~id:"clean" ~name:"clean" [ clean_sanctoral_entry ]
let clean_sanctoral_rules : (season, rank) P.rules =
- { P.band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10);
+ { P.vigil_feast = (fun _ -> None); band = (fun _ c -> match c.P.origin with P.Temporal -> 0 | P.Sanctoral -> 10);
disposition = (fun ~winner:_ ~loser:_ -> P.Commemorate P.Ordinary);
admit = (fun ~observed:_ ~temporal:_ cs -> List.map (fun (c, p, (_ : int)) -> (c, p)) cs) }
end