diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-15 01:59:11 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-15 01:59:11 +0200 |
| commit | 6b41415f17f3264454f6dcc13898948fd1902d0b (patch) | |
| tree | f8f6b21934ac203c57f84d7b07a4abfd2c8ccf42 /test/test_lectionary_ef.ml | |
| parent | 1299815ea03c2b1e4ff19d92d32e386fec24f4e4 (diff) | |
| download | colitur-6b41415f17f3264454f6dcc13898948fd1902d0b.tar.gz colitur-6b41415f17f3264454f6dcc13898948fd1902d0b.zip | |
ef(lectionary): fix round 1 -- the step-4 guard, and a miscited authority
Review re-verified all fifteen saints and all seven formularies against both
printings and found zero citation discrepancies, confirming the Gabriel
printing error and all three proper/Common reversals; the reordering was
adjudicated sound. Two Important findings, both about the guard rather than
the data.
IMPORTANT 1 -- the guard had no test, and a comment claimed it did.
`test_step4_never_diverts_a_temporal_office` asserted 2026-07-04 keeps its
ferial Mass and its comment claimed to "pin it behaviourally". It did not:
no shipped assignment names a temporal slug, so the guard is a NO-OP on
shipped data and that test passes with the guard removed. The reviewer
proved it by forcing `sanctoral_office = true` -- all 357 tests stayed
green. That mattered more than an ordinary missing test: the guard is the
structural claim the whole reordering argument leans on, and on a task with
no oracle the tests are the entire safety net.
Fixed by exposing `Commons.of_tables` (already present internally; it
applies exactly `load`'s validation, so a synthetic table is a well-formed
table with a WRONG assignment rather than malformed data the loader would
reject before the guard is reached -- and it is the constructor a future
diocesan overlay needs anyway), adding `day_with ~commons` beside `day`, and
writing test_step4_guard_refuses_a_common_assigned_to_a_ferial_slug: a
synthetic Commons assigning the Common of Abbots to
`ef-time-after-pentecost-5-saturday`, a really-observed ferial slug,
asserting 2026-07-04 still says its ferial Mass. Two checks, the first
making the second mean something -- it asserts the hostile assignment IS
present and resolvable, so the ferial Mass wins because the GUARD refused it
and not because the lookup missed. Mutation-verified: with the guard
disabled exactly one test fails, this one; before this round that mutation
reddened nothing. The old test is renamed
test_step4_leaves_a_temporal_office_alone_on_shipped_data and its comment
corrected to say it does not pin the guard.
IMPORTANT 2 -- the guard cited an authority that does not say what was
claimed. The comment said "[Validate] already asserts slug uniqueness per
liturgical year, so a sanctoral feast can never collide with a temporal
slug". validate.ml's check maps over `Temporal.office` slugs ONLY, compared
to each other across the days of one year; it says nothing about
cross-stream collision. An assumption was promoted to an asserted invariant
-- the exact defect class this project's citation discipline exists to
catch. Rewritten to state it as an assumption (no sanctoral slug carries the
`ef-` prefix every temporal slug does; 0 of 327 today), to quote the real
authority verbatim (validate.ml's own note that this "is the same assumption
the rest of this codebase already leans on"), to carry a CORRECTED marker
naming the wrong citation so it is not re-derived, and to record the failure
mode if it ever broke: benign and one-directional -- a colliding saint is
denied his Common and falls through to steps 2/3, i.e. to exactly the answer
the unguarded chain would have given him. No day gains a reading it should
not have.
MINOR -- the previous commit body led with "Blast radius, measured ... ZERO
differing lines", which is true but oversold: the CLI prints no citations, so
that sweep is STRUCTURALLY INCAPABLE of observing the reorder or any citation
value. It is a valid negative control for the sanctoral `Edit` overlay and
nothing else. The evidence for the reorder is mutation 1 (relegating step 4
to last reddens exactly the four step-4 pins) plus those pins' own explicit
expected values -- not the sweep. Correcting the record here.
MINOR -- test_commons_load_rejects_bad_data wrote four FIXED-name files into
the temp dir and removed none, colliding across concurrent runs. Replaced
with a local `with_temp_file` copied from test_lectionary.ml's own
(`Filename.temp_file` + `Fun.protect ~finally`); the four labels moved into
the assertion messages where they belonged. Verified zero leftovers after a
full run.
MINOR -- added the sharper argument for the reorder, which makes the
deviation obviously right rather than merely well-evidenced, and it now LEADS
the branch comment as warrant (1), with the Missal citation demoted to a
corroborating warrant (2): step 4 is STEP 1'S CONTINUATION. Step 1 already
runs the observed office's own proper ahead of steps 2 and 3; step 4 is that
same rule for the saints whose readings the Missal keeps in a Common instead
of printing on the celebration. Placing it last would have made the chain
internally inconsistent with code that already existed -- St Joseph beating a
competing temporal entry because his readings sit on his Celebration.t, and
St Vincent Ferrer losing to one because his sit one indirection away. Nothing
in the rubrics draws that distinction; it is an artefact of where colitur
stores the data. So the plan's ordering did not merely disagree with the
data, it contradicted step 1.
358 tests green (357 before), 359 with the exhaustive sweep.
Diffstat (limited to 'test/test_lectionary_ef.ml')
| -rw-r--r-- | test/test_lectionary_ef.ml | 106 |
1 files changed, 84 insertions, 22 deletions
diff --git a/test/test_lectionary_ef.ml b/test/test_lectionary_ef.ml index e28476d..71888e3 100644 --- a/test/test_lectionary_ef.ml +++ b/test/test_lectionary_ef.ml @@ -64,6 +64,16 @@ let day y m d = (Rite_ef.context ~lectionary:(real_lectionary ()) ~commons:(real_commons ())) (real_layer ()) date +(* Same resolution, with a caller-supplied Commons table instead of the + committed one -- the seam the whole design rests on, and the only way to + exercise step 4's guard, which is deliberately a no-op on shipped data. *) +let day_with ~commons y m d = + let date = match Date.make ~year:y ~month:m ~day:d with + | Ok x -> x | Error e -> Alcotest.fail e in + Calendar.day + (Rite_ef.context ~lectionary:(real_lectionary ()) ~commons) + (real_layer ()) date + let refs (ld : _ Liturgical_day.t) = List.map (fun c -> c.Citation.reference) ld.citations @@ -272,19 +282,64 @@ let test_step1_proper_francis_of_paola () = [ "Phil 3:7-12"; "Luke 12:32-34" ] (refs (day 2008 4 2)) -(* Step 4 must never divert a day whose observed office IS its temporal - office. The guard in [readings] makes that structural; this pins it - behaviourally on the case most at risk -- the RG 78 Saturday Office of - the BVM, which deliberately REUSES the ordinary ferial slug (see - temporal_ef.ml's [bvm_saturday_names], "Slug" paragraph). If that shared - slug were ever assigned a Common, every feria sharing it would change - Mass. 4 July 2026 is such a Saturday; it keeps its own ferial Mass. *) -let test_step4_never_diverts_a_temporal_office () = +(* A day whose observed office IS its temporal office is untouched by step 4 + on the SHIPPED data. 4 July 2026 is such a day, and the case most at risk: + it is an RG 78 Saturday Office of the BVM, which deliberately REUSES the + ordinary ferial slug (temporal_ef.ml's [bvm_saturday_names], "Slug" + paragraph). + + This test alone does NOT pin the guard -- it passes with the guard + removed, because no shipped assignment names a temporal slug. It is the + baseline the next test needs, and it is labelled as such (fix round 1, + coordinator review: its comment previously claimed to "pin the guard + behaviourally", which was false -- the reviewer forced + [sanctoral_office = true] and all 357 tests stayed green). *) +let test_step4_leaves_a_temporal_office_alone_on_shipped_data () = Alcotest.(check (list string)) - "4 July 2026 (a BVM Saturday) keeps its ferial Mass, unaffected by step 4" + "4 July 2026 (a BVM Saturday) keeps its ferial Mass" [ "1 Pet 3:8-15."; "Matt 5:20-24." ] (refs (day 2026 7 4)) +(* THE GUARD, pinned. The guard is a no-op on shipped data by design, so the + only way to exercise it is to build the table it defends against: a + SYNTHETIC [Commons.t] that assigns a Common to a really-observed FERIAL + slug. That is exactly the future-overlay mistake the guard exists to + catch -- and note the blast radius it would have, since a ferial slug is + shared by every year in which that week and weekday coincide, not by one + date. + + Built through [Commons.of_tables], so it passes the same validation the + committed file does -- this is a well-formed table with a wrong + assignment, not malformed data caught by the loader. + + TEETH (verified, fix round 1): with the guard removed this returns the + Common of Abbots' Ecclus 45:1-6 / Matt 19:27-29 and the assertion fails. + The second check is what makes the first one mean something: it proves + the hostile assignment really is present and resolvable, so the ferial + Mass wins because the GUARD refused it, not because the lookup missed. *) +let test_step4_guard_refuses_a_common_assigned_to_a_ferial_slug () = + let ferial_slug = Slug.of_string_exn "ef-time-after-pentecost-5-saturday" in + let abbots = Slug.of_string_exn "common-of-abbots" in + let hostile = + match + Lectionary_ef.Commons.of_tables + ~commons: + [ (abbots, + [ { Citation.part = Citation.First; reference = "Ecclus 45:1-6" }; + { Citation.part = Citation.Gospel; reference = "Matt 19:27-29" } ]) ] + ~assigned:[ (ferial_slug, abbots) ] + with + | Ok c -> c + | Error e -> Alcotest.failf "synthetic commons failed to build: %s" e + in + Alcotest.(check bool) + "the synthetic table really does resolve that ferial slug to a Common" true + (Lectionary_ef.commons_for ~commons:hostile ferial_slug <> None); + Alcotest.(check (list string)) + "4 July 2026 keeps its ferial Mass even so: step 4 refuses a temporal office" + [ "1 Pet 3:8-15."; "Matt 5:20-24." ] + (refs (day_with ~commons:hostile 2026 7 4)) + (* THE DATA ASSERTION, and the one that cannot drift: after Task 6, every sanctoral entry that can ever BE the observed office -- i.e. every [status Feast] entry -- either carries its own proper or has a Common @@ -416,17 +471,22 @@ let test_step4_unreachable_propers_are_present () = Written against strings rather than the committed file so the committed file stays clean. *) let test_commons_load_rejects_bad_data () = - let write name contents = - let path = Filename.concat (Filename.get_temp_dir_name ()) name in + (* [Filename.temp_file] + [Fun.protect], exactly as test_lectionary.ml's + own [with_temp_file] does it (fix round 1, coordinator review: this + used four FIXED names in the temp dir and removed none of them, so + concurrent runs collided and the files leaked). *) + let with_temp_file contents f = + let path = Filename.temp_file "commons_test" ".sexp" in let oc = open_out path in output_string oc contents; close_out oc; - path + Fun.protect ~finally:(fun () -> Sys.remove path) (fun () -> f path) in - let err name contents = - match Lectionary_ef.Commons.load (write name contents) with - | Ok _ -> Alcotest.failf "%s: expected Error, got Ok" name - | Error e -> e + let err what contents = + with_temp_file contents (fun path -> + match Lectionary_ef.Commons.load path with + | Ok _ -> Alcotest.failf "%s: expected Error, got Ok" what + | Error e -> e) in let good_common = "(c-a (((part First) (reference \"A 1:1\")) ((part Gospel) (reference \"B 2:2\"))))" in let contains needle haystack = @@ -439,15 +499,15 @@ let test_commons_load_rejects_bad_data () = (contains needle msg) in check_msg "duplicate common" "duplicate common" - (err "commons-dup-common.sexp" + (err "duplicate common" (Printf.sprintf "((commons (%s %s)) (assigned ()))" good_common good_common)); check_msg "duplicate assignment" "duplicate assignment" - (err "commons-dup-assign.sexp" + (err "duplicate assignment" (Printf.sprintf "((commons (%s)) (assigned ((s c-a) (s c-a))))" good_common)); check_msg "empty formulary" "no citations" - (err "commons-empty.sexp" "((commons ((c-a ()))) (assigned ()))"); + (err "empty formulary" "((commons ((c-a ()))) (assigned ()))"); check_msg "unknown common" "unknown common" - (err "commons-unknown.sexp" + (err "unknown common" (Printf.sprintf "((commons (%s)) (assigned ((s c-missing))))" good_common)); (* A missing file is an [Error] too, never an exception -- the contract [Lectionary.load] already makes and the reason neither is read at @@ -487,8 +547,10 @@ let suite = test_step1_proper_beats_any_common_john_of_god); ("step 1: Thomas Aquinas's proper", `Quick, test_step1_proper_thomas_aquinas); ("step 1: Francis of Paola's proper", `Quick, test_step1_proper_francis_of_paola); - ("step 4 never diverts a temporal office (BVM Saturday)", `Quick, - test_step4_never_diverts_a_temporal_office); + ("step 4 leaves a temporal office alone on shipped data (BVM Saturday)", `Quick, + test_step4_leaves_a_temporal_office_alone_on_shipped_data); + ("step 4's GUARD refuses a Common wrongly assigned to a ferial slug", `Quick, + test_step4_guard_refuses_a_common_assigned_to_a_ferial_slug); ("every observable sanctoral Feast has a proper or a Common", `Quick, test_every_observable_sanctoral_feast_has_readings); ("the Commons assignment table, exactly", `Quick, test_step4_assignment_table); |
