diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 03:37:37 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 03:37:37 +0200 |
| commit | efc1b303417388c75f27403952b506dc4d4e03d7 (patch) | |
| tree | dc96daf81b126bb03c9b453a028df2de85baf626 | |
| parent | fcfce4ba0ab4ea93837b647deaa5292300aaad5e (diff) | |
| download | colitur-efc1b303417388c75f27403952b506dc4d4e03d7.tar.gz colitur-efc1b303417388c75f27403952b506dc4d4e03d7.zip | |
test(differential): gate C1/C6 on slug family, pin the fixture's SHA-256
Fix round 1 on Task 15's review, four findings closed.
C1 and C6 previously gated on calendar date alone -- unlike every other
Layer C entry, which also checks the actual colitur slug that won. A
future sanctoral change producing an unrelated celebration with the same
diff shape (e.g. a promoted 29-31 December saint landing on C6's own
[Slug_f; Rank]) would have been silently absorbed under a citation that
has nothing to do with the real cause. Both entries now require, whenever
Slug_f is in the diff set, that colitur's own slug be a member of an
explicit closed list (jan_6_13_slug, nativity_octave_day_slugs) -- the
same style advent_feria_slug already used for C4. Constructed the exact
substitution via a temporary (unshipped) overlay Replace directive
promoting thomas-becket to Class1 Feast White; confirmed the tightened
predicate refuses to absorb it (46 unexplained rows, one per year); reverted.
C10 gained the diff-field-subset restriction every other entry already
had (it previously fired on the two pinned dates regardless of which
fields differed).
Fixed a stale doc comment ("SIX" columns; field has five constructors).
The fixture's provenance note now records its SHA-256, and the suite
asserts it as its own first test case (test_fixture_checksum), so a
hand-edited or partially re-copied fixture fails loudly instead of
silently becoming an unlabelled snapshot. Computed via the system
sha256sum binary through Sys.command plus a redirected-output temp file
-- no new library dependency, not even unix. Constructed a one-byte
fixture edit and confirmed the checksum test fails independently of the
row-comparison test; reverted.
237/237 tests green, clean-build verified.
| -rw-r--r-- | test/fixtures/lectio-ef-2005-2050.provenance | 8 | ||||
| -rw-r--r-- | test/test_differential.ml | 109 |
2 files changed, 107 insertions, 10 deletions
diff --git a/test/fixtures/lectio-ef-2005-2050.provenance b/test/fixtures/lectio-ef-2005-2050.provenance index e738484..872517b 100644 --- a/test/fixtures/lectio-ef-2005-2050.provenance +++ b/test/fixtures/lectio-ef-2005-2050.provenance @@ -5,6 +5,14 @@ Produced by the sibling project ~/git/projects/lectio at commit: 2386a45 cmd(lectio-ef-dump): new EF calendar dumper for colitur's differential oracle (branch polish-ui-and-calendar at the time; see that repo's own log) +SHA-256: 2ca3eeeda4e7a0406c4d004c1b2003fc0df671aca9af18a1b506543a721c8bac +(same digest as [fixture_sha256] in test/test_differential.ml, ASSERTED by +that suite's own "fixture SHA-256 matches its provenance note" test -- this +line documents the pin for a reader who never runs the suite; that test is +what actually enforces it. Regenerate both together, deliberately, after +re-running the exact command below -- never by copying a mismatch's actual +value back in, which would defeat the pin's purpose.) + Exact command: cd ~/git/projects/lectio && go run ./cmd/lectio-ef-dump 2005 2050 diff --git a/test/test_differential.ml b/test/test_differential.ml index aea1dac..dd601a1 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -2,11 +2,13 @@ 2005-2050 -- validation layer 3 of the design spec's five (colitur CLAUDE.md "Validation" section; layers 1-2, Types and Property, are already built and green). lectio's own EF stream is committed as a fixture - (test/fixtures/lectio-ef-2005-2050.txt, provenance in the sibling - .provenance file next to it); colitur's side is recomputed fresh from this - library on every run, through the SAME pipeline `colitur day` uses - (Colitur_kernel.Calendar over the real data/ef/sanctoral.sexp + - adjustments.sexp), not the compiled binary. + (test/fixtures/lectio-ef-2005-2050.txt, provenance -- including the + SHA-256 this file's own [test_fixture_checksum] asserts, so a hand-edit or + partial re-copy of the fixture fails loudly rather than silently becoming + an unlabelled snapshot -- in the sibling .provenance file next to it); + colitur's side is recomputed fresh from this library on every run, through + the SAME pipeline `colitur day` uses (Colitur_kernel.Calendar over the + real data/ef/sanctoral.sexp + adjustments.sexp), not the compiled binary. *** THREE STATED LIMITS THIS COMPARATOR DOES NOT PRETEND TO EXCEED *** @@ -108,6 +110,49 @@ let adjustments_path = "../data/ef/adjustments.sexp" let fixture_path = "fixtures/lectio-ef-2005-2050.txt" let allow_list_path = "../data/ef/expected-divergences.sexp" +(* The fixture's own provenance note (test/fixtures/lectio-ef-2005-2050.provenance) + records this same digest, so it is discoverable by a reader who never runs + the suite. Recorded here too, and ASSERTED (fix round 1, finding 4): a + provenance note is enough to REGENERATE the fixture but says nothing about + whether the committed bytes still match the commit they claim to come + from -- an unnoticed hand-edit or partial re-copy would silently turn the + whole oracle into an unlabelled snapshot of whatever someone last ran. + Regenerate this constant (and the provenance note's copy) together, + deliberately, after re-running the exact command the provenance note + names -- never by copying the actual value back in to make a mismatch + pass, which would defeat the point of pinning it at all. *) +let fixture_sha256 = "2ca3eeeda4e7a0406c4d004c1b2003fc0df671aca9af18a1b506543a721c8bac" + +(* Same technique tools/bootstrap_sanctoral.ml already uses for this exact + purpose (that file's own comment: shelling out to the system's + [sha256sum], not an OCaml crypto library -- Task 15's deps are frozen). + Unlike that tool, this avoids even the (already-permitted, per that + file's own comment, "already in the switch") [unix] library: [Sys.command] + plus a redirected-to-file capture needs nothing beyond the Stdlib every + dune executable already links. Depends on [sha256sum] being on PATH, + which every environment this suite has actually run in (Debian, per + CLAUDE.md) provides via coreutils; a machine without it fails this check + with a command-not-found exit code rather than silently skipping it. *) +let sha256_of_file path = + let tmp = Filename.temp_file "colitur_differential_sha256" ".txt" in + Fun.protect + ~finally:(fun () -> try Sys.remove tmp with Sys_error _ -> ()) + (fun () -> + let cmd = Printf.sprintf "sha256sum %s > %s" (Filename.quote path) (Filename.quote tmp) in + let rc = Sys.command cmd in + if rc <> 0 then Alcotest.failf "sha256sum exited %d for %s (is it on PATH?)" rc path; + let ic = open_in tmp in + let line = + try input_line ic + with End_of_file -> + close_in ic; + Alcotest.failf "sha256sum produced no output for %s" path + in + close_in ic; + match String.index_opt line ' ' with + | Some i -> String.sub line 0 i + | None -> Alcotest.failf "unexpected sha256sum output for %s: %S" path line) + let real_layer () = let layer = match Layer.load V.rank_of_sexp sanctoral_path with @@ -293,7 +338,7 @@ let strip_epiphany_index slug = (* ---------------------------------------------------------------------- *) (* Field-diff computation: applies Layers A and B, then reports exactly *) -(* which of the SIX substantive columns still differ (week is never *) +(* which of the FIVE substantive columns still differ (week is never *) (* inspected at all -- limit 3). weekday is included defensively: dates *) (* are checked 1:1 aligned before this runs, so it should never fire, and *) (* if it ever does that is real signal, not noise to normalise away. *) @@ -346,18 +391,48 @@ let sunday_iclass_slugs = let rose_sunday_slugs = [ "ef-advent-sunday-3"; "ef-lent-sunday-4" ] +(* Fix round 1, finding 1: C1 and C6 (below) originally gated on calendar + date alone, with no slug/slug-family check -- unlike every other entry + here. The reviewer constructed the failure this leaves open: if a future + sanctoral regeneration made some OTHER 29-31 December celebration win the + day (colliding coincidentally with C6's own [Slug_f; Rank] diff shape), + it would be silently absorbed under "RG 91 entry 17, Nativity Octave" -- + a citation that has nothing to do with the real cause. [subset diffs + [...]] alone was never enough; the SLUG that actually won must also be + the one each citation is about. Both lists below are exact literals (the + Nativity Octave's three colitur-only day slugs; the closed set of + offices that can legitimately observe C1's Jan 6-13 window), not + patterns -- a slug outside them fails through to [None] instead of being + absorbed. *) +let jan_6_13_slug slug = + String.equal slug "ef-epiphany" + || String.equal slug "commemoration-of-the-baptism-of-the-lord" + || List.exists (fun wd -> String.equal slug ("ef-christmas-2-" ^ wd)) weekdays + +let nativity_octave_day_slugs = + [ "ef-nativity-octave-day-5"; "ef-nativity-octave-day-6"; "ef-nativity-octave-day-7" ] + (* [layer_c_reason l c diffs] returns the [data/ef/expected-divergences.sexp] [id] this row-pair's remaining (post Layer A/B) diff set belongs to, or [None] if nothing here explains it (a genuine, uncovered failure). *) let layer_c_reason (l : row) (c : row) diffs = let m = month_of_date l.date and d = day_of_date l.date in if diffs = [] then None - else if m = 1 && d >= 6 && d <= 13 && subset diffs [ Season; Colour_f; Slug_f ] then Some "C1" + else if + m = 1 && d >= 6 && d <= 13 + && subset diffs [ Season; Colour_f; Slug_f ] + && (not (List.mem Slug_f diffs) || jan_6_13_slug c.slug) + then Some "C1" else if List.mem c.slug sunday_iclass_slugs && diffs = [ Rank ] then Some "C2" else if List.mem c.slug rose_sunday_slugs && subset diffs [ Rank; Colour_f ] then Some "C3" else if advent_feria_slug c.slug && diffs = [ Rank ] then Some "C4" else if starts_with ~prefix:"ef-lent-ember-" c.slug && subset diffs [ Slug_f; Rank ] then Some "C5" - else if m = 12 && (d = 29 || d = 30 || d = 31) && subset diffs [ Slug_f; Rank ] then Some "C6" + else if + m = 12 + && (d = 29 || d = 30 || d = 31) + && subset diffs [ Slug_f; Rank ] + && (not (List.mem Slug_f diffs) || List.mem c.slug nativity_octave_day_slugs) + then Some "C6" else if (String.equal c.slug "matthew" || String.equal c.slug "thomas") && subset diffs [ Slug_f; Colour_f ] then Some "C7" else if @@ -369,7 +444,10 @@ let layer_c_reason (l : row) (c : row) diffs = || String.equal c.slug "joseph-spouse-of-the-bl-virgin-mary") && subset diffs [ Season; Slug_f; Rank; Colour_f ] then Some "C9" - else if (String.equal l.date "2011-07-02" || String.equal l.date "2011-07-04") then Some "C10" + else if + (String.equal l.date "2011-07-02" || String.equal l.date "2011-07-04") + && subset diffs [ Slug_f; Rank; Colour_f ] + then Some "C10" else None (* ---------------------------------------------------------------------- *) @@ -423,6 +501,16 @@ let describe_unexplained (l : row) (c : row) diffs = (String.concat "," (List.map field_name diffs)) l.weekday l.season l.slug l.rank l.colour c.weekday c.season c.slug c.rank c.colour +(* Fix round 1, finding 4: the fixture's own byte content must still match + the SHA-256 its provenance note claims (lectio commit 2386a45, recorded + both here and in test/fixtures/lectio-ef-2005-2050.provenance). Checked + before anything else reads the fixture -- a drifted fixture makes every + other assertion in this suite a statement about an unlabelled snapshot, + not about the pinned lectio commit it claims to be. *) +let test_fixture_checksum () = + Alcotest.(check string) "fixture SHA-256 matches its provenance note" fixture_sha256 + (sha256_of_file fixture_path) + (* Dates align 1:1 in the same order on both streams (both are one line per civil day, 2005-01-01..2050-12-31 -- see the fixture's own provenance note and [colitur_rows_2005_2050]'s construction). A silent misalignment @@ -486,7 +574,8 @@ let test_layer_c_counts_match_citations () = let suite = ( "differential (lectio, EF, 2005-2050)", - [ Alcotest.test_case "streams are 16801 rows each, dates aligned 1:1" `Quick test_dates_align; + [ Alcotest.test_case "fixture SHA-256 matches its provenance note" `Quick test_fixture_checksum; + Alcotest.test_case "streams are 16801 rows each, dates aligned 1:1" `Quick test_dates_align; Alcotest.test_case "every difference is normalised (A/B) or cited (C) -- none unexplained" `Quick test_no_unexplained_differences; Alcotest.test_case "Layer C counts match data/ef/expected-divergences.sexp exactly" `Quick |
