From 03d3c00ccdde318207c8b4fdc456e5e64d6b4abf Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Wed, 26 Aug 2026 13:57:14 +0200 Subject: feat(of-lectionary): hand-author the Christmas Day Mass, close the gap colitur readings --rite of printed "- | -" on 25 December every year -- lectio's own 988 keys never included the Christmas DAY Mass at all, only the Vigil (already correctly noted, but left unfilled, in tools/bootstrap_lectionary_of.ml's own named_overrides comment). Sourced, not guessed: docs/research/of/olm-1981-ocr.txt:4513-4520, "16 Ad Missam in die", the same primary authority this module already cites for the cycle rules. First reading and Gospel only, matching this file's own scope everywhere else (First/Gospel is the kernel's own boundary, I7, a separate deferred task) -- Isaiah 52:7-10 / John 1:1-18 (OLM's own LONGER form; the file's LINEAGE section already discloses that short/long-form choices are not modelled anywhere in this data, so this is an application of an existing limitation, not a new one). Injected via a new hand_authored table in the generator, merged into entries before assert_reachable/Lectionary.of_entries -- there is no ini section to derive this from. One new lookup_any call (readings' own existing temporal-slug fallback, unmodified) now resolves BOTH the formulary and the citations for 25 December from the same entry, so both halves of the "citations-unresolved"/"formulary" gap close together, not just the citation half. Verified across the WHOLE domain, not assumed: test_validate_of.ml's exhaustive sweep (COLITUR_EXHAUSTIVE_SWEEP=1, all 8 416 years) now asserts the Nativity gap fires on NONE of them, down from all 8 416 before this fix -- and is kept as a live, dedicated "must never recur" counter rather than deleted. test_rite_of.ml's landmark-year check and test_lectionary_of.ml's 2026 coverage pins are updated the same way (the unresolved-day set that used to be pinned as {25 December} is now pinned as empty, not merely a count going to zero). A concrete golden pin (test_golden_of.ml) locks the exact citations through the full resolved pipeline. data/of/lectionary.sexp regenerated (771 entries, was 770); its own SHA-256 re-pinned. make check (COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force, unpiped, foreground): exit 0, 895 tests, ~536s. --- data/of/lectionary.sexp | 14 ++++--- test/test_citation_coverage_of.ml | 6 +-- test/test_golden_of.ml | 31 ++++++++++++++- test/test_lectionary_of.ml | 50 ++++++++++++++---------- test/test_rite_of.ml | 45 ++++++++++++---------- test/test_validate_of.ml | 81 ++++++++++++++++++++++++++------------- tools/bootstrap_lectionary_of.ml | 43 +++++++++++++++++++-- 7 files changed, 187 insertions(+), 83 deletions(-) diff --git a/data/of/lectionary.sexp b/data/of/lectionary.sexp index 8cc1d63..437cada 100644 --- a/data/of/lectionary.sexp +++ b/data/of/lectionary.sexp @@ -52,11 +52,11 @@ ; SHA-256: f0b527aef58b93e6ccdfbeafe52b340ed2a9d7d72805bf5083e3da04b080123c ; 988 ini sections (988 expected) -> 438 resolved (base, cycle-shape) pairs -> 435 colitur slugs mapped, 3 ini bases genuinely excluded (structurally ; unreachable -- see [excluded_bases]), 0 ini bases genuinely unmapped -; (no pattern, no override, no matching sanctoral slug) -> 770 emitted +; (no pattern, no override, no matching sanctoral slug) -> 771 emitted ; lectionary entries. ; ; COVERAGE, BOTH DIRECTIONS (Step 1 of this task's own brief): -; (1) Temporal-day gap: of the 365 days of civil year 2026, 1 produce a +; (1) Temporal-day gap: of the 365 days of civil year 2026, 0 produce a ; Rite_of.Temporal_of slug with NO entry in this file (tried the ; date-keyed route first, then flat, then both the Sunday- and ; weekday-cycle letter suffixes -- the same order @@ -76,8 +76,8 @@ ; OLM norms working as designed for a saint with no proper of ; their own, not a gap in this data. ; (4) Citation-siglum conversion (fix wave I2, 2026-08-26 review): of -; 1540 emitted (First, Gospel) citation fields, 41 distinct -; references (out of the full 1540) do not parse -- every one a +; 1542 emitted (First, Gospel) citation fields, 41 distinct +; references (out of the full 1542) do not parse -- every one a ; hyphen range that crosses a chapter boundary ("2:29-3:6"), a ; {!Colitur_citation.Parse.t} shape this parser's [part]/ ; [verse_range] types do not represent, deliberately not built @@ -134,9 +134,8 @@ ; Wisdom 11:22-12:2 ; Wisdom 2:23-3:9 ; Wisdom 7:22b-8:1 -; Temporal_of slugs (2004-2051) with no lectionary entry (1, informational -- +; Temporal_of slugs (2004-2051) with no lectionary entry (0, informational -- ; most are the Christmas-season/late-Advent date-vs-weekday gap named above): -; of-nativity ((all-saints (((part First) (reference "Revelation 7:2-4,9-14")) ((part Gospel) (reference "Mat 5:1-12")))) @@ -857,6 +856,9 @@ (of-mary-mother-of-god (((part First) (reference "Numbers 6:22-27")) ((part Gospel) (reference "Luke 2:16-21")))) + (of-nativity + (((part First) (reference "Isaiah 52:7-10")) + ((part Gospel) (reference "John 1:1-18")))) (of-nativity-vigil (((part First) (reference "Isaiah 62:1-5")) ((part Gospel) (reference "Mat 1:18-25")))) diff --git a/test/test_citation_coverage_of.ml b/test/test_citation_coverage_of.ml index 3444706..70e63ad 100644 --- a/test/test_citation_coverage_of.ml +++ b/test/test_citation_coverage_of.ml @@ -19,7 +19,7 @@ the FULL data/of/lectionary.sexp (not merely what one civil year happens to observe -- a stricter check than EF's own year-walk, and the same one tools/bootstrap_lectionary_of.ml's own generated header now runs and - discloses): 41 distinct references, out of 1540 total, EVERY ONE a + discloses): 41 distinct references, out of 1542 total, EVERY ONE a hyphenated range crossing a chapter boundary ("2:29-3:6", "1 John") -- a {!Colitur_citation.Parse.t} shape [part]/[verse_range] do not represent (a [part] is one chapter and a list of within-chapter ranges; @@ -91,9 +91,9 @@ let test_of_citations_convert_or_are_the_known_cross_chapter_residual () = cits) (Colitur_kernel.Lectionary.entries (lectionary ())); let bad = List.sort compare !bad in - Alcotest.(check int) "1540 citation fields in data/of/lectionary.sexp today -- if this changes, the \ + Alcotest.(check int) "1542 citation fields in data/of/lectionary.sexp today -- if this changes, the \ pinned residual below may need updating too, not just this count" - 1540 !total; + 1542 !total; Alcotest.(check (list string)) "the unconverted set is EXACTLY the known cross-chapter residual, no \ more and no fewer" (List.sort compare known_cross_chapter_residual) bad; diff --git a/test/test_golden_of.ml b/test/test_golden_of.ml index f40b62a..fd4f781 100644 --- a/test/test_golden_of.ml +++ b/test/test_golden_of.ml @@ -34,7 +34,10 @@ 5. All Souls (2 November). 6. The Sacred Heart (Easter+68). Plus 17 December's own date-keyed reading (Gen 49:2,8-10 / Mt 1:1-17, - fixed earlier on this branch, commit b6d0eba0). *) + fixed earlier on this branch, commit b6d0eba0), and, added by the + fix-wave-2 review (I8), 25 December's own reading (Isa 52:7-10 / + Jn 1:1-18, hand-authored from OLM 1981 -- see [test_christmas_day_ + reading]'s own header). *) module Cal = Colitur_kernel.Calendar module Layer = Colitur_kernel.Layer @@ -317,6 +320,29 @@ let test_december_17_reading () = letter" 2027 12 17 "2027-12-17 slug=of-advent-3-friday first=Genesis 49:2,8-10 gospel=Matthew 1:1-17" +(* ------------------------------------------------------------------ *) +(* 8. Christmas Day itself: fix wave I8 (final-review.md, 2026-08-25- *) +(* colitur-of-phases-3-5). lectio's own 988 keys never covered the *) +(* Christmas DAY Mass at all (only the Vigil) -- `colitur readings *) +(* --rite of` used to print "- | -" on 25 December, every year (pinned, *) +(* before this fix, by test_validate_of.ml's own exhaustive-sweep *) +(* counter: fired on all 8 416 swept years). tools/ *) +(* bootstrap_lectionary_of.ml's own [hand_authored] table now injects it *) +(* directly from OLM 1981's own "16 Ad Missam in die" *) +(* (docs/research/of/olm-1981-ocr.txt:4513-4520): First = Isaiah *) +(* 52:7-10, Gospel = John 1:1-18 (OLM's own LONGER form -- the file's own*) +(* LINEAGE section already discloses that short/long-form choices are *) +(* not modelled anywhere in this data, so this is an application of an *) +(* existing, stated limitation, not a new one). Fixed civil-date entry, *) +(* not cycle-lettered (the Christmas Day Mass does not vary by A/B/C or *) +(* I/II), so a single year is sufficient -- unlike item 7 above, there is*) +(* no cycle-letter axis to re-confirm in a second year. ------------------ *) + +let test_christmas_day_reading () = + check_readings ~msg:"2026-12-25: the Christmas Day Mass, hand-authored from OLM 1981 (Isa 52:7-10/John \ + 1:1-18) -- was \"- | -\" before fix wave I8" + 2026 12 25 "2026-12-25 slug=of-nativity first=Isaiah 52:7-10 gospel=John 1:1-18" + let suite = ( "golden-of", [ Alcotest.test_case "Ordinary Time skips a week (2026); Mary, Mother of the Church" `Quick @@ -328,5 +354,6 @@ let suite = test_annunciation_holy_week_2027; Alcotest.test_case "All Souls (2026)" `Quick test_all_souls_2026; Alcotest.test_case "the Sacred Heart, Easter+68 (2026)" `Quick test_sacred_heart_2026; - Alcotest.test_case "17 December's own date-keyed reading, two civil years" `Quick test_december_17_reading + Alcotest.test_case "17 December's own date-keyed reading, two civil years" `Quick test_december_17_reading; + Alcotest.test_case "Christmas Day's own reading (fix wave I8)" `Quick test_christmas_day_reading ] ) diff --git a/test/test_lectionary_of.ml b/test/test_lectionary_of.ml index a9fcdb1..03aaea5 100644 --- a/test/test_lectionary_of.ml +++ b/test/test_lectionary_of.ml @@ -178,15 +178,14 @@ let sha256_of_file path = don't transcribe it from generator stdout" discipline test_calendar_of_data.ml's own [test_sha256] already follows. *) let test_sha256_pinned () = - (* Fix wave I2 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the - hash changed because the header grew a new "Citation-siglum - conversion" census -- the payload (parse this test's siblings assert - against) is byte-identical; only the disclosure comment above it is - new. Re-derived with `sha256sum data/of/lectionary.sexp`, not - transcribed from generator stdout, the same discipline this test's own - header states. *) + (* Fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the + hash changed again -- ONE genuine new entry this time ("of-nativity", + the Christmas Day Mass hand-authored from OLM 1981; see + test_coverage_2026's own citation), not header-only as the I2 change + just above was. Re-derived with `sha256sum data/of/lectionary.sexp`, + same discipline. *) Alcotest.(check string) "data/of/lectionary.sexp SHA-256" - "aba7817ace8add740a5722f5fdb401a119869545eda9082e41e203df4ac27ee5" (sha256_of_file lectionary_path) + "e08207614472955974b504bfe6984ff44e882c4d8fadc778c4fa1ddce1adecf8" (sha256_of_file lectionary_path) (* lectio's OWN of-lectionary.ini SHA-256, pinned inside data/of/ lectionary.sexp's own provenance header (tools/bootstrap_lectionary_of @@ -252,7 +251,12 @@ let test_missing_source_fails_loudly () = let test_load () = let l = real_lectionary () in - Alcotest.(check int) "770 entries" 770 (List.length (Lectionary.entries l)) + (* 771, not 770: fix wave I8 (final-review.md) added ONE hand-authored + entry, "of-nativity" (the Christmas Day Mass, absent from lectio's own + source entirely -- see this file's own [test_coverage_2026] and + tools/bootstrap_lectionary_of.ml's own [hand_authored] table for the + citation and full argument). *) + Alcotest.(check int) "771 entries" 771 (List.length (Lectionary.entries l)) (* ---- coverage: a full sample civil year, both directions, PINNED ------ *) @@ -285,14 +289,18 @@ let test_coverage_2026 () = all_days_of_2026 in Alcotest.(check int) "365 days in 2026" 365 (List.length all_days_of_2026); - (* The ONE genuine gap this task's own bootstrap found and named: lectio's - 988 keys never include a Christmas DAY Mass (only the Vigil) -- see - data/of/lectionary.sexp's own provenance header and - tools/bootstrap_lectionary_of.ml's own [named_overrides] comment on - "christmas". Pinned as a NAMED set, not a bare count: a silent zero - here is exactly the failure mode the task brief warns against. *) - Alcotest.(check (list string)) "the unresolved-day set is exactly {25 December}" [ "2026-12-25" ] unresolved; - Alcotest.(check int) "364 of 365 days resolve readings" 364 (365 - List.length unresolved) + (* CLOSED, fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): + lectio's 988 keys never included a Christmas DAY Mass (only the + Vigil) -- tools/bootstrap_lectionary_of.ml's own [hand_authored] table + now injects it directly (Isa 52:7-10/John 1:1-18, OLM 1981's own "16 + Ad Missam in die", docs/research/of/olm-1981-ocr.txt:4513-4520), so + the set that used to be pinned as {25 December} is now pinned as + EMPTY -- still a NAMED set, not a bare count collapsing to zero by + coincidence: a future regression reopening this (or any other) gap + still fails loudly by naming the date, not merely by a count + mismatch. *) + Alcotest.(check (list string)) "the unresolved-day set is empty" [] unresolved; + Alcotest.(check int) "365 of 365 days resolve readings" 365 (365 - List.length unresolved) (* Same measurement, restated as "how many days resolve" rather than "how many don't" -- redundant with the count above by construction, kept @@ -308,7 +316,7 @@ let test_pinned_resolved_count () = (fun date -> match readings_on lectionary index date with Some _, _ :: _ -> true | _ -> false) all_days_of_2026) in - Alcotest.(check int) "364 days resolve readings in 2026" 364 resolved + Alcotest.(check int) "365 days resolve readings in 2026 (fix wave I8: was 364)" 365 resolved (* ---- resolution chain: unit-level spot checks -------------------------- *) @@ -471,9 +479,9 @@ let suite = Alcotest.test_case "data/of/lectionary.sexp SHA-256 pinned" `Quick test_sha256_pinned; Alcotest.test_case "lectio source SHA-256 pinned in the header" `Quick test_source_sha256_pinned_in_header; Alcotest.test_case "a missing lectio source fails loudly, not silently" `Quick test_missing_source_fails_loudly; - Alcotest.test_case "loads, 770 entries" `Quick test_load; - Alcotest.test_case "2026 coverage: exactly {25 December} unresolved" `Quick test_coverage_2026; - Alcotest.test_case "2026 coverage: 364 days resolve" `Quick test_pinned_resolved_count; + Alcotest.test_case "loads, 771 entries" `Quick test_load; + Alcotest.test_case "2026 coverage: nothing unresolved (fix wave I8)" `Quick test_coverage_2026; + Alcotest.test_case "2026 coverage: 365 days resolve" `Quick test_pinned_resolved_count; Alcotest.test_case "step 3: temporal ferial fallback" `Quick test_step3_temporal_ferial; Alcotest.test_case "step 2: sanctoral proper wins over the ferial" `Quick test_step2_sanctoral_wins; Alcotest.test_case "date_keyed_slug boundaries (OLM n. 69.3)" `Quick test_date_keyed_slug_boundaries; diff --git a/test/test_rite_of.ml b/test/test_rite_of.ml index a1892ae..5e369b8 100644 --- a/test/test_rite_of.ml +++ b/test/test_rite_of.ml @@ -77,21 +77,25 @@ let real_of_rite = Rite_of.context ~lectionary:real_of_lectionary let run year = Val.run real_of_rite real_of_layer ~year -(* data/of/lectionary.sexp's own provenance header names ONE informational - temporal-slug gap: [of-nativity] (25 December -- Christmas Day itself) - has no entry, because niedziela.pl's own source keys the Nativity's - several Masses (Vigil/Night/Dawn/Day) differently than a single flat - slug can address, and [Lectionary_of.readings] has no fallback beyond - its own three-step chain (unlike EF's RG 299 preceding-Sunday rule, - which OF's own OLM Praenotanda gives no equivalent for -- see - lectionary_of.mli's own citation). This makes {!Colitur_kernel.Validate - .run}'s ["citations-unresolved"] check fire exactly once a year, on 25 - December, on real shipped data -- a documented, measured gap in - lectionary_of's own data file (out of this task's scope to touch or - close: lectionary_of.* is excluded from this task's brief), not a defect - this module's wiring introduced. Named and filtered explicitly, never - silently dropped, so a future SECOND gap (a regression) still reddens - this test. *) +(* CLOSED, fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5). + This paragraph used to describe a LIVE gap: data/of/lectionary.sexp's + own provenance header named ONE informational temporal-slug gap, + [of-nativity] (25 December -- Christmas Day itself), because + niedziela.pl's own source never covered the Christmas DAY Mass at all + (only the Vigil), which made {!Colitur_kernel.Validate.run}'s + ["citations-unresolved"]/["formulary"] checks fire every year, on 25 + December, on real shipped data. tools/bootstrap_lectionary_of.ml's own + [hand_authored] table now injects that entry directly (Isaiah 52:7-10 / + John 1:1-18, OLM 1981's own "16 Ad Missam in die", + docs/research/of/olm-1981-ocr.txt:4513-4520) -- a primary-source + citation, not a guess, the same discipline every other hand-authored + entry in this project follows. [is_known_nativity_gap] is kept, not + deleted, but is no longer in [is_known]'s own filter below: it now + exists ONLY as an explicit, named "this must never fire again" check + ({!check_year_allowing_known_gaps}'s own dedicated assertion) -- a + regression here surfaces as an ordinary unexpected-failure test error, + not a silently-absorbed known gap, which is the whole point of moving a + predicate out of the filter once its own defect closes. *) let ends_with ~suffix s = let ls = String.length s and lx = String.length suffix in ls >= lx && String.sub s (ls - lx) lx = suffix @@ -141,18 +145,19 @@ let is_known_holy_family_fallback_gap (f : Val.failure) = let check_year_allowing_known_gaps year = let fs = run year in - let is_known f = is_known_nativity_gap f || is_known_holy_family_fallback_gap f in + (* Fix wave I8: [is_known_nativity_gap] is deliberately NOT part of + [is_known] any more -- see that predicate's own header. Only the + Holy Family fallback gap is still filtered out here. *) + let is_known f = is_known_holy_family_fallback_gap f in let unexpected = List.filter (fun f -> not (is_known f)) fs in (match unexpected with | [] -> () | fs -> Alcotest.failf "%d: %s" year (String.concat "; " (List.map Val.failure_to_string (List.filteri (fun i _ -> i < 5) fs)))); - let nativity_gaps = List.filter is_known_nativity_gap fs in Alcotest.(check int) - (Printf.sprintf "%d: exactly the two documented of-nativity checks (citations-unresolved + \ - formulary), no more" year) - 2 (List.length nativity_gaps) + (Printf.sprintf "%d: no of-nativity citations-unresolved/formulary gap (fix wave I8 -- was 2)" year) + 0 (List.length (List.filter is_known_nativity_gap fs)) (* Landmark years, the same choice test_validate.ml's own [test_landmark_years] makes for EF: both domain edges plus two ordinary diff --git a/test/test_validate_of.ml b/test/test_validate_of.ml index a8a6001..5c64710 100644 --- a/test/test_validate_of.ml +++ b/test/test_validate_of.ml @@ -14,20 +14,26 @@ down, not a replacement for Task 5's own tests (which stay as they are, including their own three pinned defects -- see below). - THE THREE KNOWN, PINNED-NOT-FIXED GAPS (Task 5's own report; do not - relitigate here, only avoid papering over them): + THE KNOWN, PINNED-NOT-FIXED GAPS (Task 5's own report; do not relitigate + here, only avoid papering over them). Originally three; now two -- see + item 2's own CLOSED note: 1. Normae n. 35(a)'s Holy Family fallback is unreached whenever Christmas Day is itself a Sunday (test_rite_of.ml's own [is_known_holy_family_fallback_gap]/ [test_holy_family_fallback_1583_known_wrong_ferial]) -- fires on roughly one year in seven, first hit at 1583. - 2. The lectionary (data/of/lectionary.sexp) was audited against only one - civil year (2026) -- surfaces here as [Validate]'s own - ["citations-unresolved"]/["formulary"] gap on 25 December every year - (test_rite_of.ml's own [is_known_nativity_gap]), and, more broadly, - as a citation-chain gap this file's own comparators (which never - touch citations/formulary at all -- see the "what this layer cannot - see" note at the end) cannot see beyond that one symptom either way. + 2. CLOSED, fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases- + 3-5). Used to read: "The lectionary (data/of/lectionary.sexp) was + audited against only one civil year (2026) -- surfaces here as + [Validate]'s own [\"citations-unresolved\"]/[\"formulary\"] gap on 25 + December every year". tools/bootstrap_lectionary_of.ml's own + [hand_authored] table now injects the Christmas Day Mass directly + (Isa 52:7-10/John 1:1-18, OLM 1981's own "16 Ad Missam in die"), so + this fires on ZERO years now, verified across the whole domain by + [test_exhaustive_domain_sweep]'s own [nativity_gap_years] counter + below -- not merely assumed from the fix. [is_known_nativity_gap] + itself is kept, as a dedicated "this must never recur" check, not + deleted. 3. St Joseph / Palm Sunday (Normae n. 56(f)) -- invisible to [Validate.run] entirely: the impeded solemnity is still transferred exactly once, to SOME later date, so no structural check @@ -98,6 +104,12 @@ let ends_with ~suffix s = let ls = String.length s and lx = String.length suffix in ls >= lx && String.sub s (ls - lx) lx = suffix +(* CLOSED, fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): + see test_rite_of.ml's own [is_known_nativity_gap] header for the + citation and the full argument (this predicate is duplicated from that + file, this file's own header already states why -- not shared through a + .mli). Kept as an explicit "this must never fire again" check, no longer + part of [is_known_gap]'s own filter below. *) let is_known_nativity_gap (f : Val.failure) = (f.Val.check = "citations-unresolved" || f.Val.check = "formulary") && ends_with ~suffix:"12-25" f.Val.date @@ -109,7 +121,10 @@ let contains ~substring s = let is_known_holy_family_fallback_gap (f : Val.failure) = f.Val.check = "anchor" && contains ~substring:"of-holy-family" f.Val.detail -let is_known_gap f = is_known_nativity_gap f || is_known_holy_family_fallback_gap f +(* Fix wave I8: [is_known_nativity_gap] deliberately excluded -- see its own + header just above. A recurrence now surfaces as an ordinary unexpected + failure everywhere this predicate is used to filter. *) +let is_known_gap f = is_known_holy_family_fallback_gap f let unexplained_failures year = List.filter (fun f -> not (is_known_gap f)) (run year) @@ -268,6 +283,12 @@ let test_exhaustive_domain_sweep () = for y = 1583 to 9998 do let fs = run y in let hf = List.exists is_known_holy_family_fallback_gap fs in + (* Fix wave I8: expected to be [false] every single year now -- kept + as a live measurement, not deleted, so a regression anywhere in + the domain increments this counter AND (via [is_known_gap] no + longer absorbing it) fails the loop below immediately, on the + first year it recurs, rather than only being caught by the + summary assertion after the full sweep completes. *) let nat = List.exists is_known_nativity_gap fs in if hf then incr holy_family_gap_years; if nat then incr nativity_gap_years; @@ -288,33 +309,39 @@ let test_exhaustive_domain_sweep () = (not (List.exists (fun f -> f.Val.check = "coverage") fs_9999)); Alcotest.(check bool) "9999: seasons check flags the truncated final year as incomplete" true (List.exists (fun f -> f.Val.check = "seasons") fs_9999); - Alcotest.(check (list string)) "9999: nothing OTHER than the documented seasons truncation (and the three \ - known, pinned gaps) fired" + Alcotest.(check (list string)) "9999: nothing OTHER than the documented seasons truncation (and the two \ + remaining known, pinned gaps) fired" [ "seasons" ] (List.sort_uniq compare (List.map (fun f -> f.Val.check) (List.filter (fun f -> not (is_known_gap f)) fs_9999))); - (* Both known gaps are real and not vacuous across the FULL domain, not - merely on the handful of years the sampled property happens to draw - -- the Nativity gap fires on literally EVERY year (25 December - always exists); the Holy Family gap fires on roughly one year in - seven (whenever 25 December is a Sunday). Pinned as a range, not an - exact count, deliberately: the exact figure is a real, computable - fact about the Gregorian calendar's own 400-year cycle, but pinning - it to the digit would make this test fail the moment a future - COLITUR_EXHAUSTIVE_SWEEP run's own domain bounds shift by even one - year at either edge, for a reason having nothing to do with - colitur's own correctness. *) - Alcotest.(check int) "the Nativity citation/formulary gap fires on every one of the 8 416 swept years" - 8416 !nativity_gap_years; + (* The Holy Family gap is real and not vacuous across the FULL domain, + not merely on the handful of years the sampled property happens to + draw -- it fires on roughly one year in seven (whenever 25 December + is a Sunday). Pinned as a range, not an exact count, deliberately: + the exact figure is a real, computable fact about the Gregorian + calendar's own 400-year cycle, but pinning it to the digit would + make this test fail the moment a future COLITUR_EXHAUSTIVE_SWEEP + run's own domain bounds shift by even one year at either edge, for a + reason having nothing to do with colitur's own correctness. *) Alcotest.(check bool) "the Holy Family fallback gap fires on a real, non-trivial fraction of years \ (roughly one in seven)" true - (!holy_family_gap_years > 1000 && !holy_family_gap_years < 1400) + (!holy_family_gap_years > 1000 && !holy_family_gap_years < 1400); + (* CLOSED, fix wave I8: the Nativity gap used to fire on literally every + one of the 8 416 swept years (25 December always exists). Now + exactly zero -- across the WHOLE domain, not merely the 4 landmark + years test_rite_of.ml's own [test_landmark_years] already checks, + which is the entire reason this dedicated exhaustive counter is kept + rather than deleted alongside the rest of the old gap-tracking. *) + Alcotest.(check int) "the Nativity citation/formulary gap fires on NONE of the 8 416 swept years (fix \ + wave I8 -- was 8 416)" + 0 !nativity_gap_years end let suite = ( "Validate (OF, real data: Rite_of.context)", - [ Alcotest.test_case "landmark years validate cleanly (modulo the three known gaps)" `Quick test_landmark_years; + [ Alcotest.test_case "landmark years validate cleanly (modulo the two remaining known gaps)" `Quick + test_landmark_years; Alcotest.test_case "year 9999 does not raise; seasons flags the clamp, nothing else (beyond the known \ gaps) fires" `Quick test_year_9999_does_not_raise; diff --git a/tools/bootstrap_lectionary_of.ml b/tools/bootstrap_lectionary_of.ml index a679b01..f94a7db 100644 --- a/tools/bootstrap_lectionary_of.ml +++ b/tools/bootstrap_lectionary_of.ml @@ -376,9 +376,12 @@ let named_overrides = [ (* Content-verified against the real reading (Isa 62:1-5/Matt 1:18-25, the genealogy/annunciation-to-Joseph narrative): this is the VIGIL Mass, not "Christmas Day" despite the bare name -- the real Christmas - DAY Mass (Isa 52:7-10/John 1:1-14) is ABSENT from lectio's own 988 - keys entirely, a genuine, notable gap (see the generator's own - coverage report). *) + DAY Mass is ABSENT from lectio's own 988 keys entirely (a genuine + gap, not a mapping bug here). CLOSED, fix wave I8: [hand_authored] + below now injects it directly (Isa 52:7-10/John 1:1-18, OLM 1981's + own "16 Ad Missam in die") -- see that table's own citation for the + full argument, including why it is not routed through THIS table + (there is no ini section to override). *) ("christmas", [ "of-nativity-vigil" ]); (* Content-verified (Sirach 24 Wisdom-personified / Eph 1 / John 1:1-18): this is the SECOND SUNDAY AFTER THE NATIVITY (Normae n.36), not a @@ -621,6 +624,38 @@ let weekday_letter = function `I -> "i" | `II -> "ii" let slug_or_die name = match Slug.of_string name with Ok s -> s | Error e -> die "%s" e +(* Fix wave I8 (final-review.md, 2026-08-25-colitur-of-phases-3-5): the + Christmas DAY Mass ("of-nativity") is genuinely absent from lectio's own + 988 ini keys -- niedziela.pl's own harvest never covered it at all, not + a mapping bug in this generator (already noted, correctly, in + [named_overrides]'s own comment on the "christmas" base above, which is + the VIGIL, not the Day). Confirmed sourceable, not merely "cannot + convert": docs/research/of/olm-1981-ocr.txt:4513-4520 ("16 Ad Missam in + die"), the same primary authority this whole module already cites for + the cycle rules (lectionary_of.mli). First reading and Gospel only, + matching this file's own scope everywhere else (data/of/lectionary.sexp + :2's own header, and {!Colitur_kernel.Validate}'s [First; Gospel] + assertion) -- OLM's own Second Reading (Heb 1:1-6) and Psalm (Ps 97) are + out of scope for the identical reason every other day's Second Reading + and Psalm are (I7, a kernel-level limitation, not this file's own). + + The Gospel (Io 1, 1-18) carries OLM's own "longior/brevior" choice + ("1-18 (longior) vel 1-5.9-14 (brevior)") -- the LONGER form is used, + the same already-disclosed limitation this file's own LINEAGE section + states for every other short/long-form pair in this data ("Nor can it + show OLM's short/long-form reading alternatives, which niedziela.pl does + not distinguish"), not a new one introduced here. + + Injected directly as a Lectionary entry, not through the ini-derived + [mapped]/[entries_of] pipeline above -- there is no ini section to + derive it FROM. Merged into [entries] before {!assert_reachable} (so it + is checked exactly like every other emitted key: "of-nativity" is a + real Rite_of.Temporal_of slug) and before {!Lectionary.of_entries}. *) +let hand_authored = + [ ( slug_or_die "of-nativity", + [ { Citation.part = Citation.First; reference = "Isaiah 52:7-10" }; + { Citation.part = Citation.Gospel; reference = "John 1:1-18" } ] ) ] + let entries_of (slug, r) = match r with | Flat cs -> [ (slug_or_die slug, cs) ] @@ -866,7 +901,7 @@ let () = let resolved = resolve_sections secs in let sanctoral_slugs = reachable_sanctoral_slugs () in let mapped, report = map_bases resolved ~sanctoral_slugs in - let entries = List.concat_map entries_of mapped in + let entries = List.concat_map entries_of mapped @ hand_authored in let temporal_slugs = reachable_temporal_slugs () in let date_keyed_slugs = reachable_date_keyed_slugs () in let uncovered_temporal = assert_reachable entries ~temporal_slugs ~sanctoral_slugs ~date_keyed_slugs in -- cgit v1.3