diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 12:44:15 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 12:44:15 +0200 |
| commit | dc00d1f730b23fa99cd15ff142ecba515a9fe0b7 (patch) | |
| tree | e40f7f003008b33104723a6d8856168bbdea7633 | |
| parent | 59fbda87d565ad9b98c2493e9703390cd988609d (diff) | |
| parent | e45ec1ccce68d9edb9655004c21861d6bfb33a9d (diff) | |
| download | colitur-dc00d1f730b23fa99cd15ff142ecba515a9fe0b7.tar.gz colitur-dc00d1f730b23fa99cd15ff142ecba515a9fe0b7.zip | |
Merge branch 'ef-holyname-rg110': the Holy Name of Jesus, RG 110
Builds the Most Holy Name of Jesus with the fallback RG 17(a) and the
calendarium both specify -- 'secus die 2 ianuarii', 'vel, ea deficiente,
die 2 ianuarii' -- which was unimplemented, so in 3619 of the domain's
8417 years a II-class feast did not exist at all. Also retags it as a
feast of the Lord per RG 91 entry 14.
Builds RG 110's inseparable Peter/Paul commemoration ('in numero
orationum computando, pro unica habeantur') across the three calendarium
pairs, and adds the 30 June companion, which was missing from lectio's
source and from missalemeum's output alike -- corroborated in both
photographic scans plus RG 124(c), and added through adjustments.sexp so
the bootstrap's provenance stands.
14627 days change, in four shapes, none unclassified. The register's
earlier 852-day estimate for RG 110 reconciles exactly as the Lent Ember
subset of one shape: 288 + 284 + 280.
RG 110(c)'s ordering was implemented backwards at first and corrected:
'huic orationi additur altera' attaches the companion to the trigger's
own oration, so it follows it, where (a) and (b) attach to the day's and
so precede everything. The 'ante omnes alias commemorationes' half
remains unimplemented and is unreachable by construction -- shape (c)
needs the trigger to lose while still being admitted, which only a
I-class day or a II-class Sunday allows, and both RG 111 caps are one.
Records a fifth way the validation layers can be green while wrong:
nothing anywhere compares commemoration order.
| -rw-r--r-- | CLAUDE.md | 132 | ||||
| -rw-r--r-- | data/ef/adjustments.sexp | 55 | ||||
| -rw-r--r-- | data/ef/expected-divergences-missalemeum.sexp | 33 | ||||
| -rw-r--r-- | data/ef/expected-divergences.sexp | 5 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.ml | 224 | ||||
| -rw-r--r-- | lib/rites/rite_ef/precedence_ef.mli | 20 | ||||
| -rw-r--r-- | lib/rites/rite_ef/temporal_ef.ml | 157 | ||||
| -rw-r--r-- | lib/rites/rite_ef/temporal_ef.mli | 11 | ||||
| -rw-r--r-- | test/test_differential.ml | 21 | ||||
| -rw-r--r-- | test/test_golden.ml | 121 | ||||
| -rw-r--r-- | test/test_oracle.ml | 105 | ||||
| -rw-r--r-- | test/test_precedence_ef.ml | 235 | ||||
| -rw-r--r-- | test/test_temporal_ef.ml | 156 |
13 files changed, 1188 insertions, 87 deletions
@@ -113,6 +113,16 @@ against missalemeum; layer 5 pins ~30 dates. the week column. It also **shares colitur's own lineage**: colitur's data was bootstrapped from lectio, so an error both inherited is invisible here. Proven: Holy Thursday was violet in both because both were wrong. +- **Nothing anywhere compares commemoration ORDER** (found `ef-holyname-rg110` + task, fix round 1, RG 110's own shape-(c) ordering bug): layer 3 doesn't compare + commemorations at all (above); layer 4's own `identity_diff` sorts both sides + into a multiset before comparing; layer 5's own `describe` (test_golden.ml) + sorts its `comms` field too, deliberately, so a golden pin's string comparison + is not accidentally order-sensitive where nothing textual requires it to be. + The ONLY place in the whole suite that asserts commemoration order is + `test_precedence_ef.ml`'s own `admit_cases` table (`Alcotest.(check (list + string))`, unsorted) — proven by mutation: reverting RG 110(c)'s own + trigger/companion order left every layer green except that one table. - **Layer 4's OBSERVED-identity gap is CLOSED** (2026-08-13, branch `ef-rg112-rg110`, register §6.2): it used to compare the observed day's rank and colour and stop there — never whether it is actually the RIGHT day. Holy @@ -218,11 +228,15 @@ ferias, four Ember sets, Rogations, `anchors`) · `precedence_ef` (the full RG 9 commemorations RG 108–112, transfers RG 96–98) · `rite_ef` (the bundle). **Data**: `data/ef/sanctoral.sexp` (322 entries, bootstrapped from lectio, SHA-256 -in its provenance header) · `data/ef/adjustments.sexp` (overlay) · two cited -allow-lists, `expected-divergences.sexp` (5 active entries, vs lectio — C1, C6, -C8, C14, C15; several more closed and recorded in the register, not deleted) and -`expected-divergences-missalemeum.sexp` (12 active, vs the oracle). Fixtures -live in `test/fixtures/` with asserted SHA-256s. +in its provenance header) · `data/ef/adjustments.sexp` (overlay — `Add` as well +as `Suppress`/`Edit`: RG 110's own 30 June companion, `commemoration-of-st-peter`, +is genuinely missing from lectio's own source, not merely from colitur's +bootstrap, so it is hand-authored here rather than upstream) · two cited +allow-lists, `expected-divergences.sexp` (6 active entries, vs lectio — C1, C6, +C8, C14, C15, C16; several more closed and recorded in the register, not deleted) +and `expected-divergences-missalemeum.sexp` (12 active, vs the oracle — M12 +closed/M19 opened this task, net count unchanged). Fixtures live in +`test/fixtures/` with asserted SHA-256s. **CLI**: `colitur easter <year>`, `temporal <year>`, `day <year>`. @@ -253,9 +267,10 @@ dune exec colitur -- day 2026 | head - **Plan 4 — OF rite module** (proves `RITE` generalizes) → the lectionary bootstrap and citations → full output/rendering → hardening and a first tag. - **All three behaviour items below are now RESOLVED** (RG 16(a) and + **All four behaviour items below are now RESOLVED** (RG 16(a) and commemoration identity, closed on branch `ef-rg16a`; Holy Family/RG 112(a) - and observed identity, closed on branch `ef-rg112-rg110`) — kept here as + and observed identity, closed on branch `ef-rg112-rg110`; Holy Name of + Jesus/RG 110, closed on branch `ef-holyname-rg110`) — kept here as the record of what the five layers, taken together, used to sleep through, and as the shape a future gap of the same kind would need to be caught by. @@ -267,7 +282,7 @@ triage, and the whole-branch review — is in workspace is deliberately kept, because it and the register corrections exist nowhere in git (`docs/` is gitignored). -**The three behaviour items, in order:** +**The four behaviour items, in order:** 1. **RG 16(a) — RESOLVED (RG16(a) task, branch `ef-rg16a`, 2026-08-12; ONE FIX ROUND of review after the first pass — see register §6.0 for the full, @@ -372,37 +387,80 @@ nowhere in git (`docs/` is gitignored). was expected.** The differential's own C1 (the 6-13 January blanket) lost exactly those 7 (of the 1 220) rows within its 2005–2050 window and they were split into their own new cited entry (C15), not silently - re-absorbed — the same discipline the task brief demanded. Not built: - RG 110 (inseparable Peter/Paul, still open, M12) — out of this task's own - dispatched scope despite the branch name. Holy Name of Jesus (RG 17(a)) - has the identical "generic-Sunday-slug masking a real named feast" shape - Holy Family had — flagged as a new open item, not fixed here (no live - collision exists for it in today's data, so nothing is currently WRONG - about it the way Holy Family's own Baptism collision was). **A SECOND, - more severe Holy Name gap, found in review (fix round 1, coordinator - finding 9): unlike RG 17(b)'s own 7–13 January window (never empty), - RG 17(a)'s narrower 2–5 January window CAN be empty of a Sunday, and the - calendarium carries an explicit fallback ("vel, ea deficiente, die 2 - ianuarii") colitur does not implement at all — 3 619 of 8 417 years in - the domain (independently computed, matched exactly) have NO Holy Name - office whatsoever, a genuine missing II-class feast on 43% of years, not - merely an unnamed one. Recorded, not fixed — out of this task's own - dispatched scope. - -**Structural items, cheapest to fix before OF exists:** + re-absorbed — the same discipline the task brief demanded. Not built at + the time: RG 110 (inseparable Peter/Paul, still open, M12) — out of this + task's own dispatched scope despite the branch name. Holy Name of Jesus + (RG 17(a)) had the identical "generic-Sunday-slug masking a real named + feast" shape Holy Family had, PLUS a second, more severe gap (no RG 17(a) + fallback for its own 2–5 January window when empty of a Sunday — 3 619 of + 8 417 domain years). **Both are now RESOLVED — see item 4 below.** +4. **Holy Name of Jesus (RG 17(a)) + RG 110 — RESOLVED (2026-08-13, branch + `ef-holyname-rg110`; see register §6.3 for the full account).** Holy Name + gained the `subject = Lord` tag Holy Family already had (RG 91 entry 14), + plus a genuinely new office: RG 17(a)'s own fallback, *"secus die 2 + ianuarii"* — 2 January carries the feast whenever no Sunday falls 2–5 + January that year, tagged and ranked identically to the Sunday shape (ONE + feast, per the Mass propers' own single heading covering both dates, both + scans). Before this fix colitur emitted no Holy Name office at all in + 3 619 of 8 417 domain years — a genuine missing II-class feast, not + merely an unnamed one. A real asymmetry the fix's own synthetic + precedence tests found and kept honest rather than forced: a losing + Holy-Name-SUNDAY is RG 109(a)-privileged and survives RG 111(a)'s cap; a + losing Holy-Name-FALLBACK is not (2 January genuinely is not a Sunday, no + other RG 109 category names it) — both correct readings of RG 109/111's + own closed lists, no live witness for either today. + RG 110 (*"In Officio et Missa S. Petri semper fit commemoratio S. + Pauli, et vicissim... pro unica habeantur"*) gained a THIRD sub-clause + this register had not transcribed before, (c) — the same inseparable + commemoration also fires when one Apostle is admitted merely AS a + commemoration, not only when he is the day's own office. Three real + pairs in the 1962 calendar (25 January, 22 February, 30 June); the third + had no companion candidate anywhere — a genuine gap in lectio's own + source data AND in missalemeum's own oracle output, not only a colitur + bootstrap miss — closed via `data/ef/adjustments.sexp`'s own `Add` + directive. Built in `Precedence_ef.admit` (`rg110_additions`), layered on + AFTER `admit`'s own four RG 111 branches decide their ordinarily-capped + result, uncapped and additional, never competing for a slot. + **Blast radius, measured (`git archive` pre-change binary vs HEAD, full + 1583–9999 sweep, diffed): 14 627 days, ALL FOUR predicted shapes, zero + unclassified** — 3 619 the Holy Name fallback itself; 3 533 Paul wrongly + excluded from Chair of St Peter's own day by a competing privileged + feria (RG 110 shapes (a)/(b) — supersedes an earlier 852-day estimate + that measured only a delta between two older commits, not the rule's + full scope); 593 Chair of St Peter admitted only as an ordinary + commemoration, Paul entirely absent (shape (c) — a real 7% of the + domain, not a corner case); 6 882 the new 30-June companion. The lectio + differential needed no RG 110 change at all (it does not compare + commemorations); the oracle allow-list needed M12 removed, M15 widened + by one date (a newly-exposed instance of its own pre-existing limit), + and a new M19 for the 30-June gap. + **CORRECTED, fix round 1: RG 110(c)'s own ordering was inverted on all + 593 shape-(c) days.** *"Huic orationi additur altera"* (the companion is + added TO the trigger's own oration) means the trigger comes first, the + companion follows — the original build prepended the companion + uniformly in both shapes, right for (a)/(b) (nothing in the list to + order against) but backwards for (c). Fixed in `rg110_additions` + (splice the companion in immediately after its own trigger for shape + (c) only); the wrong-order unit test is re-pinned. New, permanent + blind spot found and recorded: **nothing in this suite compares + commemoration order except that one unit table** — see "know what each + layer cannot see" above. M19's own predicate was also strengthened to + check commemoration IDENTITY, not merely presence (the same C6/C14 + failure mode, proven by fabricating a second companion and watching the + whole suite stay green pre-fix). -4. **`Record` and `Liturgical_day` both claim to be "the single stable output +5. **`Record` and `Liturgical_day` both claim to be "the single stable output schema".** `Record` cannot express what the engine now computes (no observed celebration, no commemorations, no transfers), has **no test file**, and is used only by the legacy `colitur temporal` path — `day` hand-formats instead. Plan 2's carried item (add a `cycle` field for OF's Sunday A/B/C and weekday I/II) is still open and now costlier: it must pass through `Temporal.t`, which is embedded in the sexp-derived `Liturgical_day.t`. -5. **`Temporal.RITE` and `Rite.t` are two competing abstractions.** The module +6. **`Temporal.RITE` and `Rite.t` are two competing abstractions.** The module type still exists and `temporal_ef` still satisfies it, but it carries none of `rules`, `anchors`, `season_runs`, `transfer_target` — satisfying it now proves almost nothing. `Rite.t` is the load-bearing one. -6. **EF-shaped things still in "rite-agnostic" kernel code**: `validate.ml` +7. **EF-shaped things still in "rite-agnostic" kernel code**: `validate.ml` hardcodes Sunday as the week start; `Liturgical_day.transferred_in` is an `option` justified by RG 96; `Precedence.privilege` is defined by RG 111; `Repose` is EF vocabulary emitted by nothing. Each is one field short of the @@ -417,17 +475,11 @@ Boniface 14 May, Evaristus, Theodore); and `romanus`, which should not exist on lineage, not two independent sources. **Unbuilt, recorded**: the Sacred Triduum has no proper office (it uses ordinary -Passiontide ferial slugs); RG 91 entry 27's BVM Saturday Office; RG 110's -inseparable commemorations; Major Litanies (25 April); the Rogation-Wednesday -commemoration; Holy Name of Jesus (RG 17(a)) still carries `subject = Temporal` -like an ordinary Sunday, the same identity gap Holy Family had until this task, -found while closing it (register §6.2) but not itself fixed — no live collision -exists for it in today's data; **Holy Name of Jesus also has no RG 17(a) fallback -for its own 2–5 January window when it is empty of a Sunday (unlike RG 17(b)'s -7–13 January, never empty) — 3 619 of 8 417 domain years have no Holy Name office -at all, found in review (fix round 1, finding 9), a genuine missing II-class -feast, not merely an unnamed one.** Allow-list entries M11 and M13 are `verdict -open` by design. +Passiontide ferial slugs); RG 91 entry 27's BVM Saturday Office; Major Litanies +(25 April); the Rogation-Wednesday commemoration; RG 112(b)/(c)/(d) (only (a) +has a live witness this codebase's data can construct). Allow-list entries M11 +and M13 are `verdict open` by design. Holy Name of Jesus (RG 17(a)) and RG 110 +(inseparable Peter/Paul commemorations) are **RESOLVED — see item 4 above.** ## How to work here diff --git a/data/ef/adjustments.sexp b/data/ef/adjustments.sexp index e33b4eb..740180c 100644 --- a/data/ef/adjustments.sexp +++ b/data/ef/adjustments.sexp @@ -120,8 +120,61 @@ ; `data/ef/sanctoral.sexp` is generated and the source is upstream. Logged ; as a lectio-side generator/data defect in docs/research/rules-register.md ; for a future generator fix, not merely patched here silently. +; +; `Add commemoration-of-st-peter` -- NEW (ef-holyname-rg110 task, RG 110). +; A genuine DATA GAP in lectio's own source, not merely a colitur bootstrap +; miss: `~/git/projects/lectio/internal/caldata/tridentine-calendar.ini` has +; no 30 June entry for this at all (checked directly, `grep -n '30 June'` +; equivalent), so there is nothing for `tools/bootstrap_sanctoral.ml` to +; have picked up even correctly. RG 110's own text (docs/research/rules- +; register.md §4, Caput XIV): "In Officio et Missa S. Petri semper fit +; commemoratio S. Pauli, ET VICISSIM" -- in the Office and Mass of EITHER +; Peter or Paul, a commemoration of the OTHER is always made -- confirmed a +; THIRD time by the calendarium's own June table, BOTH photographic scans, +; word for word: "In Commemoratione S. Pauli Ap., III classis. / +; Commemoratio S. Petri Ap." -- exactly the SAME pattern already present in +; the base bootstrap for the other two RG 110 pairs (25 January's +; `conversion-of-st-paul` + `peter`; 22 February's `chair-of-st-peter` + +; `paul`), just missing here. `Precedence_ef.admit`'s own RG 110 mechanism +; (precedence_ef.ml) looks this slug up by name whenever +; `in-commemoratione-sancti-pauli-apostoli` is the day's own observed office +; -- a distinct slug from `peter` (25 January), because `peter` is already a +; DIFFERENT dated entry and this codebase's slugs are date-canonical, one +; date per slug (design spec §3.2). Class3/Commemoration_only/Saint, the +; SAME shape as `peter`/`paul`'s own two entries; names copied verbatim from +; `peter`'s own entry (the identical saint, the identical commemoration +; text, on a second date) -- NOT independently sourced from a missalemeum +; title (missalemeum shows no commemoration on this date at all, checked +; directly against its own fixture rows for both 2026 and 2027: neither +; year's `In Commemoratione Sancti Pauli Apostoli` row carries any +; commemoration -- missalemeum has the SAME gap this directive closes, +; verdict colitur in data/ef/expected-divergences-missalemeum.sexp's own +; M19). Colour Red: this ENTRY's own field has no independent primary-source +; pin (a bare "Commemoratio" line's own colour is not independently stated +; by the calendarium the way a full feast's class-and-colour line is -- the +; same honestly-flagged inference shape this file's own `romanus`/`eusebius- +; confessor` history already documents elsewhere in this project), chosen to +; match `paul`'s own entry and the HOST DAY's own Red -- and the host day's +; own Red IS now primary-sourced (fix round 1, coordinator finding F4), not +; merely RG 117's generic apostle/martyr default: RG 124(c), scan-verified +; word for word, `missale-romanum-1962.pdf`: "124. Item adhibetur color +; ruber in Officio et Missa de festis: ... c) Commemorationis S. Pauli +; Apostoli (30 iunii)" -- red is ALSO used in the Office and Mass of the +; feasts of ... (c) the Commemoration of St Paul the Apostle (30 June) -- +; naming this exact date by number. Inert either way today +; (Commemoration_only entries are never the OBSERVED celebration, so this +; entry's OWN colour is never printed by the current pipeline, the same +; note `Edit eusebius-confessor` above makes for its own colour field). ((id ef-adjustments) (directives ((Suppress vigil-of-christmas) (Edit most-holy-name-of-mary ((Set_subject Bvm))) - (Edit eusebius-confessor ((Set_colour White)))))) + (Edit eusebius-confessor ((Set_colour White))) + (Add + ((date (Fixed (month 6) (day 30))) + (cel + ((slug commemoration-of-st-peter) + (names + ((en "St. Peter") (pl "\197\155w. Piotra, Aposto\197\130a"))) + (rank Class3) (status Commemoration_only) (colour Red) + (subject Saint) (citations ()) (layer ef-universal)))))))) diff --git a/data/ef/expected-divergences-missalemeum.sexp b/data/ef/expected-divergences-missalemeum.sexp index 15f9cf2..0584dfa 100644 --- a/data/ef/expected-divergences-missalemeum.sexp +++ b/data/ef/expected-divergences-missalemeum.sexp @@ -48,6 +48,25 @@ ; covers only rows where [Observed_identity_unresolved] is the SOLE ; disagreement. ; +; ef-holyname-rg110 task (RG 110, the inseparable Peter/Paul commemoration -- +; PART of RG 91 entry 27's neighbour list this file's own header names at +; line 15 as unbuilt; RG 91 entry 27 itself, the BVM-on-Saturday office, +; remains unbuilt, still M2 below): [M12] CLOSED, REMOVED (same shape as +; M17's own removal above, not re-adjudicated) -- 22 February 2027 no +; longer diverges on [Comm_count], since colitur now admits Paul +; unconditionally alongside whatever else the day's own privileged +; commemoration is (precedence_ef.ml's own [rg110_additions]). [M15]'s own +; date list gained a 20th entry, 2027-02-22 -- M12's own former date, now +; reaching the comparator's IDENTITY axis for the first time (count +; agrees), and landing on M15's own PRE-EXISTING, unrelated limit (one of +; the two admitted commemorations is temporal-origin and unresolvable) -- +; see M15's own note for the full account. [M19] is NEW: RG 110's other +; direction (Paul's own office commemorating Peter, not Peter's own office +; commemorating Paul), 30 June, both years -- missalemeum has the SAME gap +; this task found in lectio's own source data (checked directly, not +; assumed), so this is verdict colitur, not colitur short a feature the way +; M12 used to be. +; ; FIVE MORE REMOVED, ef-rebootstrap (2026-08-12), same shape as M17's own ; removal above -- not re-adjudicated to a different verdict, just gone, ; because the underlying divergence stopped occurring: M4 (second @@ -142,11 +161,6 @@ (verdict open) (note "This entry previously read \"No RG paragraph found to support this\" and verdict colitur -- WRONG, found by fix-round-1 review: RG 67/109(c) and the calendarium's own \"Com. octavae Nativitatis\" note both confirm a real commemoration is due here. It is ALSO a genuine REGRESSION Task 16's own commemoration-eligibility fix introduced: before that fix, colitur unconditionally commemorated the day's own losing temporal candidate (\"ef-christmas-0-<weekday>\", the generic Christmastide ferial slug 26-28 December falls through to since temporal_ef.ml's [named] does not cover those three dates), which happened to keep commemoration PRESENCE agreeing with missalemeum even though the underlying reasoning was wrong (a Task 16 review-round-1 finding elsewhere, F1/F2 fixed it for the general case) -- and Task 16's fix (and this fix round's RG 23-26 correction of it) both correctly stop commemorating that generic candidate, since [ferial_rank]'s Christmastide catch-all gives it Class4, which RG 26 never commemorates. The catch: RG 67 says days WITHIN the octave are II class, not IV -- the generic ferial construction 26-28 December falls through to has the WRONG rank for this specific window (a rank bug, not merely a missing-candidate one; 29-31 December already get the correct Class2 via [named]'s own \"ef-nativity-octave-day-{5,6,7}\" branch, so this is narrowly a 26-28-December gap). Fixing it properly also has to get RG 69 right (\"De dominica infra octavam Nativitatis Domini... semper fit Officium cum commemoratione FESTI FORTE OCCURRENTIS\" -- when the Sunday falls in this window, the SUNDAY is observed with a commemoration of THE FEAST ITSELF, Stephen/John/the Innocents by name -- NOT the generic octave placeholder), so a naive \"treat the octave commemoration as one more competing candidate\" fix would get the Sunday case backwards (confirmed by working through it: it would make the octave placeholder outrank the named saint for the Sunday's own privileged slot, when RG 69 names the SAINT, not the octave, for that slot). This needs either new architecture (a commemoration that attaches to whichever of Stephen/John/the Innocents is observed, not a competing candidate the way RG 92-95's occurrence framework models everything else) or a more careful RG-69-aware fix than this task attempted -- out of scope for this fix round. Verdict changed from colitur to open: neither side is confidently right without that fix, and the previous entry's \"colitur is correct, nothing to implement\" framing was itself the risk this review flagged. Register §6 open item.") (expected_rows 4)) - ((id M12) - (citation "RG 110: \"In Officio et Missa S. Petri semper fit commemoratio S. Pauli, et vicissim\" -- in the Office and Mass of either Peter or Paul, a commemoration of the other is ALWAYS made (\"the inseparable commemoration\"), uncapped by RG 111's ordinary admission count") - (verdict missalemeum) - (note "Unimplemented in this codebase. 22 February 2027 (Chair of St Peter): colitur commemorates only the privileged Lent feria; missalemeum shows that AND \"St. Paul\". A genuine, primary-cited feature gap -- register §6 open item. Narrow: the only row in this window where it becomes visible against this comparator's axes (see the task report for the other Peter/Paul offices checked and found clean).") - (expected_rows 1)) ((id M13) (citation "OPEN -- not adjudicated. 1962 calendarium's March table: \"Feria VI post dominicam I Passionis: Commemoratio septem Dolorum B. Mariæ Virg.\" (confirmed real, every year) -- but RG 91's plain table (entry 11-13, Joseph, I class, vs entry 22, an ordinary III-class Passiontide feria) gives Joseph the day outright, with no RG 96 collision requiring a transfer, so the primary text found here does not by itself explain missalemeum's result") (verdict open) @@ -155,8 +169,8 @@ ((id M15) (citation "docs/research/rules-register.md §4's own \"THE MAPPING, AND EXACTLY WHAT IT CANNOT RESOLVE\" note (test_oracle.ml's own header) -- not an RG citation, a comparator LIMIT: Rite_ef.Temporal_ef never sets an English [names] field on any candidate it builds (temporal_ef.ml has no \"en ...\" string anywhere in it)") (verdict unresolvable) - (note "19 rows where colitur's own admitted commemoration is TEMPORAL-origin (an impeded Advent or Lent feria) -- there is no English name on colitur's side to compare against missalemeum's title text at all, so identity cannot be attempted, only reported as unresolved. Not a rubric dispute and not a data gap either engine is wrong about. Every oracle title on these 19 rows reads as a plain prose description of the SAME temporal ferial slot colitur independently computes for that date (e.g. 2 Dec 2026: oracle \"Feria IV after I Sunday of Advent\" against colitur's own ef-advent-1-wednesday; 4 Dec 2027: oracle \"Sabbato after I Sunday of Advent\" against ef-advent-1-saturday) -- suggestive corroboration, recorded for a human reader, NOT claimed as a verified match: this comparator has no mechanism to verify English prose against an opaque slug. Fixing this at the root (an English name on every temporal-cycle candidate) is a data/lectionary-bootstrap task, not something this comparator can do for itself -- register §6, neighbouring the RG 113 tie-break section, records it as an open item. The 19 dates: 2026-02-24, 2026-03-19, 2026-03-25, 2026-12-02, 2026-12-03, 2026-12-04, 2026-12-07, 2026-12-11, 2026-12-21, 2027-02-24, 2027-12-02, 2027-12-03, 2027-12-04, 2027-12-06, 2027-12-07, 2027-12-11, 2027-12-13, 2027-12-16, 2027-12-21.") - (expected_rows 19)) + (note "20 rows where at least one of colitur's own admitted commemorations is TEMPORAL-origin -- there is no English name on colitur's side to compare against missalemeum's title text at all, so identity cannot be attempted for the WHOLE row, only reported as unresolved (test_oracle.ml's own [identity_diff]: any unresolved name bails the whole comparison out, regardless of how many others resolve cleanly). Not a rubric dispute and not a data gap either engine is wrong about. 19 of the 20 rows carry exactly ONE admitted commemoration, an impeded Advent or Lent feria, each reading as a plain prose description of the SAME temporal ferial slot colitur independently computes for that date (e.g. 2 Dec 2026: oracle \"Feria IV after I Sunday of Advent\" against colitur's own ef-advent-1-wednesday; 4 Dec 2027: oracle \"Sabbato after I Sunday of Advent\" against ef-advent-1-saturday) -- suggestive corroboration, recorded for a human reader, NOT claimed as a verified match: this comparator has no mechanism to verify English prose against an opaque slug. ADDED, ef-holyname-rg110 task: 2027-02-22 -- the closed M12's own date (immediately above this entry's own former position), now admitting TWO commemorations (RG 110's own [paul], sanctoral-origin and resolvable, AND the pre-existing [ef-lent-2-monday], temporal-origin and not) -- newly reaches identity comparison only because RG 110 fixed the [Comm_count] mismatch that used to mask it, and lands on this SAME pre-existing limit, not a new one. Fixing this at the root (an English name on every temporal-cycle candidate) is a data/lectionary-bootstrap task, not something this comparator can do for itself -- register §6, neighbouring the RG 113 tie-break section, records it as an open item. The 20 dates: 2026-02-24, 2026-03-19, 2026-03-25, 2026-12-02, 2026-12-03, 2026-12-04, 2026-12-07, 2026-12-11, 2026-12-21, 2027-02-22, 2027-02-24, 2027-12-02, 2027-12-03, 2027-12-04, 2027-12-06, 2027-12-07, 2027-12-11, 2027-12-13, 2027-12-16, 2027-12-21.") + (expected_rows 20)) ((id M16) (citation "1962 calendarium's March table (same primary text as M13's own citation): \"Feria VI post dominicam I Passionis: Commemoratio septem Dolorum B. Mariae Virg.\" -- a fixed commemoration of Our Lady's Seven Sorrows for the Friday of Passion Week, every year, confirmed real") (verdict missalemeum) @@ -167,3 +181,8 @@ (verdict unresolvable) (note "373 of the 730 days in this window carry this shape alone (M1/M2/M3/M16 absorb a further 26 where it fires ALONGSIDE their own pre-existing citation -- see each entry's own widened note; 373+26=399 is the axis's own full unresolved population). CORRECTED, fix round 1 (coordinator finding 8) -- the full breakdown, precisely: of 730 days, 331 are RESOLVED (colitur's own observed celebration carries a name) -- 330 resolved-and-MATCHING, 1 resolved-and-MISMATCHED (M13, Joseph vs the Seven Sorrows, already its own entry above) -- and 399 are UNRESOLVED (colitur's own name is [None]), split between this entry's own 373 and the 26 absorbed elsewhere. 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 (rank 2/white on both sides, purely coincidental). This count PIN is what stands guard against that population growing silently in either direction: an implementation change that made MORE days temporal-origin-and-unresolvable, or fewer, changes 373 and fails the pin, even though the axis itself cannot say WHICH specific day moved or why. The overwhelming majority of days whose observed celebration is TEMPORAL-origin (an ordinary Sunday, a feria, a movable named feast including Holy Family itself) never carry an English name on colitur's side -- not a rubric dispute and not a data gap either engine is wrong about, a LIMIT of this comparator, 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\"). Gated on the diff SHAPE alone (exactly, and only, observed-identity-unresolved), not a literal date list the way every other entry in this file is -- at this population size a date list would itself be the \"pattern that could silently widen\" this file's own header warns against, for the opposite reason a date RANGE is normally risky: the predicate (colitur's own resolved name is [None]) is the precise, falsifiable evidence, the same shape M2's own title-substring predicate already uses instead of enumerating dates, just keyed on presence-of-a-name rather than a title string. Building an English name onto every temporal-cycle candidate (a data/lectionary-bootstrap task, Plan 4, out of this task's scope -- register §6, the same open item M15's own note already tracks) would resolve this entry to Matched for every one of these days it does not instead expose as a REAL divergence (Holy Family's own two dates in this window, 2026-01-11 and 2027-01-10, are counted here, not separately -- the name comparison genuinely cannot distinguish \"Holy Family\" from any other unnamed Sunday, so proving Holy Family's own correctness rests on the golden pins and the differential's own C15, not this axis). Derived directly from the comparator's own failure output (test_layer_m_counts_match_citations), not hand-counted first and cross-checked after: 373.") (expected_rows 373)) + ((id M19) + (citation "RG 110's OTHER direction: \"In Officio et Missa S. Petri semper fit commemoratio S. Pauli, ET VICISSIM\" -- the SAME rule M12 used to cite, applied here to Paul's own office commemorating Peter, not Peter's own office commemorating Paul; confirmed a third time by the calendarium's own June table, both photographic scans, word for word: \"In Commemoratione S. Pauli Ap., III classis. / Commemoratio S. Petri Ap.\"") + (verdict colitur) + (note "30 June, both years in this window (2026-06-30, 2027-06-30): colitur now shows +commemoration-of-st-peter (data/ef/adjustments.sexp's own `Add` directive) where missalemeum shows nothing at all -- not even Peter listed under \"displaced\", meaning missalemeum's own engine never constructs a candidate for this commemoration in the first place, the SAME shape this task found in lectio's own source data (`~/git/projects/lectio/internal/caldata/tridentine-calendar.ini` has no 06-30 companion entry either -- an upstream data gap both reference engines share, not a colitur bootstrap miss). Verdict colitur, the same shape as M1/M3/M8/M10 above (each already \"missalemeum does not implement X\"): the calendarium's own text is unconditional and confirmed on both photographic scans, and colitur's own machinery for the other two RG 110 pairs (25 January, 22 February) already produces the identical shape correctly. Diff shape [Comm_presence] alone: rank/colour/observed-identity all already agreed before this fix (the OBSERVED celebration on both dates is unchanged, `in-commemoratione-sancti-pauli-apostoli`, sanctoral-origin with a resolvable name matching missalemeum's own title exactly on both rows) -- only the new commemoration's PRESENCE is new, so identity comparison is never even reached (this file's own header: identity is only attempted once presence and count already agree). 2, one per year, independently re-derived against the fixture's own two 06-30 rows before being set here, not assumed from the citation's own two-pair symmetry.") + (expected_rows 2)) diff --git a/data/ef/expected-divergences.sexp b/data/ef/expected-divergences.sexp index 283c3bc..261e900 100644 --- a/data/ef/expected-divergences.sexp +++ b/data/ef/expected-divergences.sexp @@ -67,3 +67,8 @@ (verdict colitur) (note "ef-rg112-rg110 task: SPLIT OUT of C1's own blanket 6-13 January window, not a new kind of divergence -- the same discipline the now-closed C13 already established for this exact date (C1's own note above), applied again because building Holy Family reopened the identical shape C13 used to cover, for a different underlying reason. Before this task, colitur had no Holy Family office: on these seven dates (13 January when it is itself a Sunday -- 2008, 2013, 2019, 2030, 2036, 2041, 2047), colitur's generic Sunday fallback lost outright to the fixed Commemoration of the Baptism of the Lord (already subject Lord in the rebootstrapped data, RG16(a)'s existing mechanism), producing the SAME slug lectio's own tridentine-calendar.ini shows there (`commemoration-of-the-baptism-of-the-lord`) -- only Season differed, inside C1's own shape. Holy Family (RG 17(b)) now correctly outranks the fixed Baptism (RG 91 entry 14's own \"primum mobilia, deinde fixa\" -- {!Rite_ef.Precedence_ef.entry_14_movable_band}) and excludes it as a commemoration entirely (RG 112(a), directly confirmed by the Holy Family Mass propers' own more specific 13-January rubric, both photographic scans: \"Si festum S. Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S. Familiae, sine commemoratione Baptismatis D.N.I.C., et sine commemoratione dominicae\"), so colitur's own slug on these 7 dates changes to `ef-time-after-epiphany-sunday-1` (Holy Family's own -- unchanged from the plain Sunday key it always carried, temporal_ef.ml's own comment on why) -- a genuine SLUG divergence against lectio, which has no Holy Family at all and still shows the fixed Baptism observed. Gated on the literal 7-date list AND colitur's own slug (the same identity guard C1/C6/C8/C14 already apply, fix round 1's own finding 1). Derived from the OCaml comparator's own failure output directly (test_layer_c_counts_match_citations), not by hand-counting first and cross-checking after: 7.") (expected_rows 7)) + ((id C16) + (citation "RG 17(a) (\"festum Ss.mi Nominis Iesu, celebrandum dominica quae occurrit a die 2 ad 5 ianuarii, SECUS DIE 2 IANUARII\") + the calendarium's own January table, both photographic scans, word for word: \"Dominica inter octavam Nativitatis Domini et Epiphaniam, vel, EA DEFICIENTE, die 2 ianuarii: Sanctissimi Nominis Iesu, II classis\"") + (verdict colitur) + (note "ef-holyname-rg110 task: RG 17(a)'s own fallback, previously unbuilt (register §6.2's own open item -- colitur emitted no Holy Name office at all in a year with no Sunday 2-5 January, not merely an unnamed generic Sunday the way Holy Family's own pre-fix gap was). 2 January in a year with no Sunday in that window now carries the feast (`ef-holy-name`, class-2, subject Lord) instead of the ordinary Christmastide ferial fallback (`ef-christmas-1-<weekday>`/`ef-christmas-0-<weekday>` in lectio's own naming, class-4) -- a genuine SLUG AND RANK divergence against lectio, which has no fallback for this window at all (`~/git/projects/lectio/internal/caldata/tridentine-calendar.ini` carries no 01-02 entry; checked directly, not assumed) and shows the plain ferial slug on every one of these dates regardless. Gated on the literal date list AND colitur's own slug (the same identity guard C1/C6/C8/C14/C15 already apply, fix round 1's own finding 1). 20 of the 46 years in the fixture's own 2005-2050 span have no Sunday 2-5 January -- independently re-derived against `date -d <year>-01-0{2,3,4,5} +%u` for every year in range (not the register's domain-wide 3,619-of-8,417 figure divided down; a fresh count over this file's own narrower window), then cross-checked against the OCaml comparator's own failure output before being set here: 20.") + (expected_rows 20)) diff --git a/lib/rites/rite_ef/precedence_ef.ml b/lib/rites/rite_ef/precedence_ef.ml index 374e9a4..5a2d307 100644 --- a/lib/rites/rite_ef/precedence_ef.ml +++ b/lib/rites/rite_ef/precedence_ef.ml @@ -514,7 +514,18 @@ let privilege_of (c : Vocab_ef.rank Precedence.candidate) : Precedence.privilege let is_temporal = c.Precedence.origin = Precedence.Temporal in let open Vocab_ef in (* (a) RG 109(a) (§4): "of a Sunday" -- the same slug marker RG 33's - [impedes_vigil] already reads to answer "is this candidate a Sunday". *) + [impedes_vigil] already reads to answer "is this candidate a Sunday". + + FRAGILITY, named here because this is the decision point (fix-round + re-review, F5): [is_sunday_slug] is a SUBSTRING match, so the Holy Name + of Jesus obtains this privilege on its Sunday shape only because its + slug happens to read "ef-holy-name-sunday". That is a naming accident, + not a cited rule. The real warrant is RG 17's own closing paragraph + (quoted in full at temporal_ef.ml's [holy_name_sunday]): "Haec festa + locum tenent dominicae occurrentis cum omnibus iuribus et privilegiis" + -- these feasts hold the occurring Sunday's place WITH ALL ITS RIGHTS + AND PRIVILEGES, of which RG 109(a)'s privilege is one. A slug rename + would silently drop it while every test stayed green. *) if is_sunday_slug slug then Precedence.Privileged (* (b) RG 109(b) (§4): "of a I-class day" -- the candidate's own rank. In this codebase's current disposition rules the ONLY way a @@ -1039,6 +1050,191 @@ let rec take n = function still the pair, not the triple; only the INPUT carries [band]. *) let drop_band (c, p, (_ : int)) = (c, p) +(* RG 110 (docs/research/rules-register.md §4, Caput XIV, "De dierum + liturgicorum occurrentia perpetua"), full text, both photographic scans, + word for word: "110. In Officio et Missa S. Petri semper fit + commemoratio S. Pauli, et vicissim. Haec commemoratio dicitur + inseparabilis; et duae orationes adeo in unam coalescere censentur ut, in + numero orationum computando, pro unica habeantur. Proinde: a) in Officio + S. Petri aut S. Pauli, oratio alterius Apostoli additur, ad Laudes et ad + Vesperas, sub unica conclusione, orationi diei, absque antiphona et + versu; b) in Missa S. Petri aut S. Pauli, oratio alterius Apostoli + additur, sub unica conclusione, orationi diei; c) quoties vero oratio + unius Apostoli addenda est ad modum commemorationis, huic orationi + additur altera immediate, ante omnes alias commemorationes." -- in the + Office and Mass of St Peter, a commemoration of St Paul is ALWAYS made, + and vice versa. This commemoration is called INSEPARABLE, and the two + orations are held to coalesce into one so much that, in counting the + number of orations [RG 111's own admission cap], they are counted AS + ONE. Accordingly: (a)/(b) in the Office/Mass OF Peter or Paul [i.e. when + one of them is the day's own observed office], the other Apostle's + oration is added, under a single conclusion, to the oration of the day; + (c) whenever the oration of ONE Apostle must be added AS A + COMMEMORATION [i.e. when one of them is not the day's own office but is + itself only being admitted as an ordinary/privileged commemoration + elsewhere], the OTHER is added to it immediately, BEFORE ALL OTHER + COMMEMORATIONS. + + Three real pairs in this codebase's data, all three confirmed on both + photographic scans (register §4): 25 January's [conversion-of-st-paul] + + [peter]; 22 February's [chair-of-st-peter] + [paul]; 30 June's + [in-commemoratione-sancti-pauli-apostoli] + [commemoration-of-st-peter] + (data/ef/adjustments.sexp's own `Add` directive -- lectio has no + equivalent entry at all, a genuine upstream data gap, not merely a + colitur bootstrap miss). 29 June ([sts-peter-paul]) needs none: it is + already the JOINT feast, not one Apostle's office alone. [peter]/[paul] + already existed as ordinary [Commemoration_only] sanctoral candidates + before this task (Task 10's bootstrap) -- part of RG 110's own machinery, + just wired into the ORDINARY, CAPPED RG 111 admission contest instead of + this rule's own UNCAPPED one, which is the defect this closes: measured + (1583-9999, `tools/`'s own throwaway scan, reproduced and discarded per + this task's own report) at 3,533 years where [chair-of-st-peter] is + observed but [paul] loses the day's single non-Sunday-II-class slot to a + competing privileged Advent/Lent/Ember feria (RG 109(e)/(d)), and a + FURTHER 593 years of shape (c) below, where [chair-of-st-peter] itself + loses outright to an ordinary (ordinary, i.e. non-I-class) II-class + Sunday and is admitted only AS a commemoration -- [paul] was entirely + absent in every one of those 593 years too, before this fix. 25 + January/30 June never show shape (c) at all: both trigger candidates are + [Class3], below RG 111(b)'s "de festo II classis" floor, so neither can + ever be admitted as a mere ordinary commemoration on a II-class Sunday + in the first place -- confirmed by the same scan, 0 instances either + way, not assumed absent. + + [rg110_companion_slug] reads ONLY the candidate's own slug -- the same + convention every other not-an-RG-citation lookup in this file uses + ({!universal_layer}, {!vigil_suffix}, {!nativity_octave_prefix}) -- not + [subject]/[rank]/anything else, because "which slug pairs with which" is + RG 110's own closed, three-pair list, not a general property [band]/ + [disposition] could derive structurally the way (say) [is_sunday_slug] + derives Sunday-ness from a naming convention. A rename of any of the six + slugs involved (either side of a pair) has nowhere else to be caught but + here -- flagged the same way {!annunciation_slug} already is. *) +let rg110_companion_slug (slug : string) : string option = + if slug = "chair-of-st-peter" then Some "paul" + else if slug = "conversion-of-st-paul" then Some "peter" + else if slug = "in-commemoratione-sancti-pauli-apostoli" then Some "commemoration-of-st-peter" + else None + +(* Looks [companion_slug] up directly in [comms] -- the SAME full candidate + pool {!admit} itself sorts from, before any cap is applied, so this finds + the companion regardless of whether the NORMAL admission process would + have kept it (it never would: {!band} always ranks a [Commemoration_only] + candidate at {!unclassified}, worse than literally any real table entry, + so a companion competing on its own merits never survives a contested + slot -- RG 110's entire point is that it should not have to). Returns the + ORIGINAL triple's candidate/privilege pair, unrebuilt, for the same + physical-equality reason {!drop_band} and this function's own top comment + already document -- reusing the [privilege] {!disposition}/{!privilege_of} + already computed for it rather than inventing a new tag here. *) +let rg110_find_companion comms companion_slug = + List.find_map + (fun (c, p, (_ : int)) -> + if Slug.to_string c.Precedence.cel.Celebration.slug = companion_slug then Some (c, p) else None) + comms + +(* RG 110's own uncapped addition, applied to [normal] -- the day's ALREADY- + COMPUTED, ORDINARILY-CAPPED admission result (every branch of {!admit} + below, unchanged otherwise). Two triggers, covering RG 110(a)/(b) and + RG 110(c) respectively, in this function's own comment above -- and, + fix round 1, TWO DIFFERENT ORDERINGS, not one, because the two shapes' + own primary texts say different things about where the companion goes: + + - [observed] itself is one of the three trigger slugs (shape (a)/(b): + Peter's or Paul's own Office/Mass IS today's day) -- the companion is + looked up directly and PREPENDED to [normal]. (a)/(b)'s own text, + "oratio alterius Apostoli additur ... orationi diei" (the OTHER + Apostle's oration is added ... TO THE DAY'S OWN oration), has nothing + in [normal] to order the companion AGAINST in the first place: the + day's own oration is [observed], not a member of this list, so + leading the list is simply where an item with nothing to be "before" + or "after" inside it goes -- not itself a citation for a POSITION + the primary text does not describe a list position for at all. + - one of [normal]'s OWN members is a trigger slug (shape (c): Peter's or + Paul's own office lost the day outright but is itself being admitted + as an ordinary/privileged commemoration of whatever else won) -- the + companion is looked up the same way, but SPLICED IN IMMEDIATELY AFTER + its own trigger, not prepended to the whole list. RG 110(c)'s own + text, word for word: "quoties vero oratio unius Apostoli addenda est + ad modum commemorationis, HUIC ORATIONI ADDITUR ALTERA immediate, ante + omnes alias commemorationes" -- "huic orationi" (dative, "to THIS + oration") refers back to "oratio unius Apostoli" (the trigger's OWN + oration, the one just named as "addenda... ad modum commemorationis") + -- so "additur altera" (the OTHER is added) means the companion is + added TO the trigger's own oration, i.e. FOLLOWS it, not precedes it; + the pair as a BLOCK then precedes "omnes ALIAS commemorationes" + ("alias" = OTHER, unrelated ones -- cannot include the pair's own + first member). CORRECTED, fix round 1: the original version of this + function prepended in BOTH shapes uniformly, citing (c)'s own "ante + omnes alias" for shape (a)/(b) too by analogy -- which is fine for + WHERE the pair sits relative to unrelated commemorations, but wrongly + also reordered the PAIR'S OWN internal order in shape (c), producing + `paul, chair-of-st-peter` where the primary text requires + `chair-of-st-peter, paul` (verified against BOTH photographic scans, + word for word, no divergence to adjudicate). Confirmed live: every one + of the 593 shape-(c) days measured in this task's own report emitted + the companion FIRST before this fix; RG 111's own admission branches + never build a second, unrelated commemoration alongside a shape-(c) + trigger in today's data (RG 111(b) admits exactly one candidate on a + II-class Sunday, the only day shape (c) ever occurs), so this + splice-after-trigger behaviour is Total the same way {!take}/ + {!drop_band} are, but has no OTHER live witness to also prove the + "ante omnes alias" half of (c) against. + + Both shapes can never fire together on one call ({!Precedence.resolve} + only ever calls {!Precedence.rules.admit} once per date, and all three + trigger slugs are distinct FIXED calendar dates -- 25 January, 22 + February, 30 June -- so at most one of [observed]/[normal]'s own members + is ever a trigger for a given call). + + A companion already present in [normal] on its own merits (structurally + unreachable today -- {!unclassified}'s own comment above -- but not + provably so for every future rite/data shape) is not added a second + time: [List.exists] guards both branches. + + NEW BLIND SPOT, fix round 1: no validation layer in this codebase + compares commemoration ORDER at all -- the lectio differential does not + compare commemorations (its own "limit 1"); the oracle's own + [identity_diff] sorts both sides into a multiset before comparing; + and [describe] (test_golden.ml) sorts its own comms field too, for the + same reason presence/identity checks should not be order-sensitive by + accident. (CORRECTED, fix-round re-review: this previously also named + {!Colitur_kernel.Record}. Record.t has NO commemorations field at all -- + see record.mli -- so it neither sorts nor carries them.) This function's own [admit_cases] unit + test (test_precedence_ef.ml) is therefore the ONLY place in this + codebase's test suite where commemoration order is asserted at all -- + flagged here, and in CLAUDE.md's own "know what each layer cannot see" + section, as a genuine, permanent limit, not merely this fix's own gap. *) +let rg110_additions (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) + ~(observed : Vocab_ef.rank Precedence.candidate) + (normal : (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list) : + (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list = + let already_has slug = + List.exists (fun (c, _) -> Slug.to_string c.Precedence.cel.Celebration.slug = slug) normal + in + match rg110_companion_slug (Slug.to_string observed.Precedence.cel.Celebration.slug) with + | Some companion_slug when not (already_has companion_slug) -> ( + (* Shape (a)/(b): prepend -- see this function's own comment above. *) + match rg110_find_companion comms companion_slug with + | Some companion -> companion :: normal + | None -> normal) + | _ -> + (* Shape (c): splice each trigger's companion in immediately after it + -- {!List.concat_map} rather than a fold, so a trigger with no + companion found (defensive; {!rg110_find_companion} returning + [None]) simply passes through unchanged, and a member that is not + a trigger at all ([rg110_companion_slug] returns [None]) is never + touched. *) + List.concat_map + (fun ((c, _) as pair) -> + match rg110_companion_slug (Slug.to_string c.Precedence.cel.Celebration.slug) with + | Some companion_slug when not (already_has companion_slug) -> ( + match rg110_find_companion comms companion_slug with + | Some companion -> [ pair; companion ] + | None -> [ pair ]) + | _ -> [ pair ]) + normal + let admit ~(observed : Vocab_ef.rank Precedence.candidate) ~(temporal : Vocab_ef.rank Precedence.candidate) (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) : @@ -1073,14 +1269,22 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) is_sunday_slug (Slug.to_string temporal.Precedence.cel.Celebration.slug) in let open Vocab_ef in - match (observed_rank, day_is_sunday) with - | Class1, _ -> + (* RG 110's own uncapped additions are layered on AFTER this match -- + {!rg110_additions}'s own top comment -- never inside any one branch: + the three trigger slugs can each reach [observed] via any rank/Sunday + combination the table admits (a Class2 fixed feast, here, but nothing + in {!rg110_additions} itself assumes that), so computing it once, + uniformly, over whatever this match already decided is both simpler + and safer than duplicating the same lookup into every branch. *) + let normal = + match (observed_rank, day_is_sunday) with + | Class1, _ -> (* RG 111: "I class: none save one privileged." Ordinary commemorations never get a slot at all on a I-class day, no matter how many are due; at most one privileged one does, the highest-precedence one (RG 113: {!band}'s own table order) if several are. *) (match List.filter is_privileged sorted with [] -> [] | best :: _ -> [ drop_band best ]) - | Class2, true -> + | Class2, true -> (* RG 111(b), primary text, RE-VERIFIED word for word against the scan (final fix wave; this sentence is the sole textual basis for the shipped rank-floor fix below, and the register's own §4 "RG 111" @@ -1121,16 +1325,18 @@ let admit ~(observed : Vocab_ef.rank Precedence.candidate) match List.filter (fun (c, _, _) -> c.Precedence.cel.Celebration.rank = Class2) sorted with | [] -> [] | best :: _ -> [ drop_band best ])) - | Class2, false -> + | Class2, false -> (* RG 111: "other II class: one" -- no privilege-override clause here, unlike the Sunday case immediately above, so the day's one slot goes to whichever candidate outranks the rest by RG 113's own table-of-precedence order ({!band}), privileged or not. *) (match sorted with [] -> [] | best :: _ -> [ drop_band best ]) - | (Class3 | Class4), _ -> - (* RG 111: "III-IV class: at most two" -- by RG 113's table order, same - as the non-Sunday II-class case, just with room for two. *) - List.map drop_band (take 2 sorted) + | (Class3 | Class4), _ -> + (* RG 111: "III-IV class: at most two" -- by RG 113's table order, same + as the non-Sunday II-class case, just with room for two. *) + List.map drop_band (take 2 sorted) + in + rg110_additions comms ~observed normal (* Task 11: RG 96 -- where an impeded I-class feast lands (docs/research/ rules-register.md §4, "Transfer/translation"). [band] decides who is diff --git a/lib/rites/rite_ef/precedence_ef.mli b/lib/rites/rite_ef/precedence_ef.mli index 6a96c4e..765b573 100644 --- a/lib/rites/rite_ef/precedence_ef.mli +++ b/lib/rites/rite_ef/precedence_ef.mli @@ -243,6 +243,26 @@ val september_ember_prefix : string - [observed] a [Class3] or [Class4] day: at most two, by {!band}'s table order alone. + ADDED, ef-holyname-rg110 task: RG 110 (docs/research/rules-register.md + §4, Caput XIV) -- "In Officio et Missa S. Petri semper fit + commemoratio S. Pauli, et vicissim... pro unica habeantur" -- whenever + [observed], or one of the candidates the FOUR cases above already admit, + is one of RG 110's own three trigger slugs (25 January's + [conversion-of-st-paul], 22 February's [chair-of-st-peter], 30 June's + [in-commemoratione-sancti-pauli-apostoli]), its companion Apostle + ([peter]/[paul]/[commemoration-of-st-peter] respectively) is looked up + directly in [comms] and PREPENDED to whatever the four cases above + already produced -- UNCAPPED, i.e. added on top of, not competing for, + any of those cases' own admission limits ("pro unica habeantur": the + pair is held as ONE for RG 111's own counting purposes, so it never + consumes a second ordinary/privileged slot). This is layered on AFTER + the four cases above decide the day's ordinarily-capped result, uniformly + regardless of which of the four produced it -- see the .ml's own + [rg110_additions] for the full citation, the measured blast radius, and + why the two lookups (observed itself, and each already-admitted + candidate) together cover both of RG 110's own textual triggers, + (a)/(b) and (c). + CORRECTED, Task B (branch ef-rg16a): this used to sort by [Vocab_ef.rank] (RG 8's coarse four-class "dignity") and break same-rank ties alphabetically by slug -- an engineering convention with no diff --git a/lib/rites/rite_ef/temporal_ef.ml b/lib/rites/rite_ef/temporal_ef.ml index 1ce9de9..bbd88ec 100644 --- a/lib/rites/rite_ef/temporal_ef.ml +++ b/lib/rites/rite_ef/temporal_ef.ml @@ -104,6 +104,56 @@ let christ_the_king y = sunday_on_or_before (mk y 10 31) date against it without reaching into [week_origin]'s implementation. *) let holy_family_sunday y = Date.add_days (sunday_on_or_before (mk y 1 6)) 7 +(* RG 17(a) (Caput III, "De Dominicis"; full text on [holy_family_sunday]'s + own citation above): "festum Ss.mi Nominis Iesu, celebrandum dominica quae + occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- the feast of the + Most Holy Name of Jesus, celebrated on the Sunday falling 2-5 January, + OTHERWISE [on] 2 January [itself]. [None] here means no such Sunday exists + that year -- [holy_name_fallback_date]'s own fallback branch is for + exactly that case; see its own citation for why the window CAN be empty + (unlike (b)'s 7-13 January window, which never is). + + Formula: the Sunday on or before 5 January is in [2,5] iff it is not + before 2 January -- the window is only 4 days wide, so there is at most + one Sunday in it either way. *) +let holy_name_sunday y = + let s = sunday_on_or_before (mk y 1 5) in + if on_or_after s (mk y 1 2) then Some s else None + +(* RG 17(a)'s own "secus die 2 ianuarii" clause, and the calendarium's + January table, BOTH photographic scans, word for word (docs/research/ + rules-register.md; also confirmed in the Mass propers' own heading for + this feast, both scans, Caput "Sanctissimi Nominis Iesu": "DOMINICA a die + 2 ad diem 5 ianuarii occurrenti, VEL, SI HAEC DEFECERIT, DIE 2 IANUARII" -- + the SUNDAY occurring 2-5 January, OR, IF THIS IS LACKING, [on] 2 January): + *"Dominica inter octavam Nativitatis Domini et Epiphaniam, vel, EA + DEFICIENTE, die 2 ianuarii: Sanctissimi Nominis Iesu, II classis"* -- the + Sunday between the Octave of the Nativity and Epiphany, or, THAT FAILING, + 2 January. Unlike [holy_family_sunday]'s 7-13 January window (which can + never be empty of a Sunday), this 2-5 January window CAN be, and is in + 3,619 of the 8,417 years in [1583, 9999] (a plain weekday check, + independently cross-checked against `date -d`) -- [temporal]'s own + dispatch applies this fallback date, tagged exactly like the Sunday shape + (subject Lord, the same Latin name), only when [holy_name_sunday y] is + [None] for that date's own civil year. *) +let holy_name_fallback_date y = mk y 1 2 + +(* The ONE Latin title both of RG 17(a)'s two shapes (the Sunday and the 2 + January fallback) carry -- ONE feast, RG 91's own "primum mobilia, deinde + fixa" language notwithstanding (see [temporal]'s own fallback-branch + comment for why both shapes band identically at entry 14's MOVABLE half, + not split across the two halves by year). Bound once so the two [temporal] + branches that use it cannot silently drift apart, the same discipline + [holy_family_sunday] already gets from being named instead of re-derived. + Both photographic scans, word for word, twice over (the calendarium's own + January table AND the Mass propers' own running header/heading, this + file's own citations on [holy_name_sunday]/[holy_name_fallback_date] + above): "Sanctissimi Nominis Iesu". No English name, deliberately -- the + SAME "would read the oracle it is compared against" reasoning + [holy_family_sunday]'s own [names] comment below gives for Holy Family. *) +let holy_name_names = + Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctissimi Nominis Iesu") ] + let same a b = Date.compare a b = 0 (* Named temporal days: the I-class feasts of the Lord (RG 91 entries 1 and 3), @@ -533,15 +583,103 @@ let temporal d = item on Holy Name of Jesus, RG 17(a)) -- this is a targeted addition for the one day this task built, not a claim that the gap is closed generally. *) + (* RG 17(a) (this file's own [holy_name_sunday], full citation + there): the SECOND Sunday a year this branch must not + leave [subject = Temporal] -- added alongside Holy + Family's own check above, not replacing it (the two + windows, 2-5 and 7-13 January, never overlap, so at most + one of the two conditions is ever true for a given [d]). + Guarded on [holy_name_sunday y = Some d] rather than only + "is [d] in [2,5] January": every OTHER Sunday in that + range must stay unnamed, and in a year where a DIFFERENT + day of the window is the one true Sunday, [d] itself is + never reached by [sunday_slug] as a Sunday to begin with + (weekday alone already excludes it) -- but writing the + check this way, against the SAME independently-computed + date [holy_name_sunday] returns rather than a bare month/ + day range, is what test_temporal_ef.ml's own anchor test + below cross-checks against. + + ADDED, fix round 1 (coordinator finding F4): the Sunday + shape's "takes the Sunday's own place, no commemoration of + it" treatment is settled at RG LEVEL, not only in Holy + Name's own Mass propers (this file's own [holy_name_sunday] + citation already has that quote) -- RG 17's own CLOSING + paragraph, immediately after its own lettered list + (a)-(e), scan-verified: "Haec festa locum tenent dominicae + occurrentis cum omnibus iuribus et privilegiis; de + dominica, proinde, nulla fit commemoratio" -- "these + feasts" (plural, covering the WHOLE list (a)-(e), Holy Name + included) "hold the place of the occurring Sunday with all + rights and privileges; of the Sunday, therefore, no + commemoration is made". A second, independent primary + source for the same conclusion the propers-level quote + already gives, not a new claim. + + FRAGILE DERIVATION, noted per fix round 1 (coordinator + finding F5): the mechanism that actually grants a LOSING + Holy-Name-Sunday its RG 109(a) privilege when outranked + (Precedence_ef.privilege_of's own (a) branch, {!Precedence + _ef.is_sunday_slug}) reads it off the "-sunday" SUBSTRING + in the slug ("ef-holy-name-sunday") -- a naming convention, + not a citation of RG 17's own text quoted immediately + above, which is the actual warrant ("these feasts hold the + place of the occurring Sunday WITH ALL ITS RIGHTS AND + PRIVILEGES" -- RG 109(a)'s own "of a Sunday" privilege is + one of those rights). Right answer, fragile path: a future + rename of this slug family would silently drop the + privilege with no compiler or test failure pointing here. + No live witness exists to test it either way (no + fixed-date candidate ever outranks Holy Name in today's + data -- test_precedence_ef.ml's own synthetic + [test_class1_feast_inside_holy_name_window_end_to_end] + proves the SYNTHETIC case only). *) let subject, names = if same d (holy_family_sunday y) then ( Colitur_kernel.Subject.Lord, Colitur_kernel.Names.of_list [ (Colitur_kernel.Lang.of_string_exn "la", "Sanctae Familiae Iesu, Mariae, Ioseph") ] ) + else if holy_name_sunday y = Some d then (Colitur_kernel.Subject.Lord, holy_name_names) else (Colitur_kernel.Subject.Temporal, Colitur_kernel.Names.empty) in build ~subject ~names ~season:s ~slug ~colour ~rank ~week:(week d) () - | None -> ( + | None -> + (* RG 17(a)'s own fallback, "secus die 2 ianuarii" (full + citation on [holy_name_fallback_date] above): ONLY when + [holy_name_sunday y] is [None] for [d]'s own civil year -- + i.e. 2-5 January genuinely has no Sunday that year -- does + 2 January itself carry the feast; every other year, 2 + January is an ordinary Christmastide feria, exactly what + [christmastide_feria_slug] below already computes for it. + Checked here, ahead of that generic fallback, the same way + [named]'s own fixed dates are checked ahead of everything + ferial -- NOT folded into [named] itself, because unlike + every one of [named]'s ~20 entries this one is + CONDITIONAL on a per-year fact ([holy_name_sunday y] = + [None]) that [named]'s bare [Date.t -> ... option] shape + has no way to express without threading [subject] through + every one of its other branches too -- the same reason + Holy Family, immediately above, is not one of [named]'s + outputs either (that branch's own comment). + + Season Christmastide (RG 72-73: 2 January is always within + it); rank/colour/subject/names identical to the Sunday + shape immediately above -- ONE feast, {!holy_name_names}'s + own comment on why both shapes share it. Band-classified + at entry 14's MOVABLE half regardless of which shape fired + this year (Precedence_ef.band; both shapes are built + through this same [temporal] function, hence [origin = + Temporal] either way) -- RG 91's "primum mobilia, deinde + fixa" split is a property of the TABLE ENTRY a feast + occupies, and RG 17(a) names ONE feast with a fallback + clause, not two differently-classified feasts that happen + to share a Mass formulary; the Mass propers' own heading + (docs/research/rules-register.md) states both shapes under + the identical title for exactly this reason. *) + if same d (holy_name_fallback_date y) && holy_name_sunday y = None then + build ~subject:Colitur_kernel.Subject.Lord ~names:holy_name_names ~season:Christmastide + ~slug:"ef-holy-name" ~colour:Colour.White ~rank:Class2 ~week:(week d) () + else ( match christmastide_feria_slug d with | Some slug -> build ~season:s ~slug ~colour:(season_colour s) ~rank:(ferial_rank d s) @@ -608,7 +746,19 @@ let temporal d = 4-tuple), but it is exactly the same kind of independently-computed anchor this list exists to guard -- an accidental drift in [holy_family_sunday]'s own [+7] would silently move Holy Family without - this guard catching it, same as any other entry here. *) + this guard catching it, same as any other entry here. + + Holy Name of Jesus (RG 17(a)) is the SAME shape, restated per-year rather + than as a single fixed pair: exactly ONE of its two slugs exists for any + given [y] ([holy_name_sunday y] is [Some] xor [None]), so exactly one + entry -- "ef-holy-name-sunday" at that Sunday, or "ef-holy-name" at + [holy_name_fallback_date y] -- is appended below, matching whichever + shape [temporal] itself will actually build that year. An erosion of + EITHER branch is still caught: {!Validate.run} calls [anchors] for both + the civil year a date falls in and the one before it (a liturgical year + straddles two), so across any two consecutive years both shapes are + exercised regardless of which one civil year [y] itself happens to + land on. *) let anchors y = let easter = Computus.gregorian_easter y in let off n = Date.add_days easter n in @@ -633,6 +783,9 @@ let anchors y = ("ef-corpus-christi", off 60); ("ef-sacred-heart", off 68); ("ef-christ-the-king", christ_the_king y) ] + @ (match holy_name_sunday y with + | Some d -> [ ("ef-holy-name-sunday", d) ] + | None -> [ ("ef-holy-name", holy_name_fallback_date y) ]) (* Compile-time check that this module satisfies the kernel's rite contract. *) module _ : Colitur_kernel.Temporal.RITE = struct diff --git a/lib/rites/rite_ef/temporal_ef.mli b/lib/rites/rite_ef/temporal_ef.mli index 68fe64e..5e85111 100644 --- a/lib/rites/rite_ef/temporal_ef.mli +++ b/lib/rites/rite_ef/temporal_ef.mli @@ -21,6 +21,17 @@ val christ_the_king : int -> Date.t fallback for it), so this needs none. *) val holy_family_sunday : int -> Date.t +(** RG 17(a): the Sunday falling 2-5 January in civil year [y], the Most Holy + Name of Jesus's own primary date -- [None] when no such Sunday exists + that year (3,619 of the 8,417 years in [1583, 9999]), unlike + {!holy_family_sunday}'s window, which never is. *) +val holy_name_sunday : int -> Date.t option + +(** RG 17(a)'s own "secus die 2 ianuarii" fallback: 2 January in civil year + [y]. Only actually carries the feast when {!holy_name_sunday} [y] is + [None] for that year -- {!temporal} is what applies that condition. *) +val holy_name_fallback_date : int -> Date.t + (** The named temporal days: I-class feasts of the Lord, vigils, and days within the Octave of the Nativity. Returns (season, slug, colour, rank). Carries no week of its own -- {!temporal} computes it uniformly via {!week} for diff --git a/test/test_differential.ml b/test/test_differential.ml index 94adb2c..bea1bf4 100644 --- a/test/test_differential.ml +++ b/test/test_differential.ml @@ -500,6 +500,22 @@ let joseph_annunciation_collision_dates_2005_2050 = [ "2008-04-01"; "2035-04-03" let holy_family_baptism_collision_dates_2005_2050 = [ "2008-01-13"; "2013-01-13"; "2019-01-13"; "2030-01-13"; "2036-01-13"; "2041-01-13"; "2047-01-13" ] +(* C16 -- ef-holyname-rg110 task: RG 17(a)'s own fallback (2 January, "secus + die 2 ianuarii"), for every civil year 2005-2050 with no Sunday 2-5 + January -- independently re-derived against `date -d <year>-01-0{2..5} + +%u`, not transcribed from the register's domain-wide figure (see + data/ef/expected-divergences.sexp's own C16 note for the full citation + and the derivation). lectio has no equivalent fallback at all (checked + directly against tridentine-calendar.ini, which carries no 01-02 entry), + so it shows the plain Christmastide ferial slug on every one of these + dates, class-4 -- colitur's own `ef-holy-name`, class-2, is a genuine + [Slug_f; Rank] divergence, not a naming synonym Layer A could absorb. *) +let holy_name_fallback_dates_2005_2050 = + [ "2006-01-02"; "2007-01-02"; "2008-01-02"; "2012-01-02"; "2013-01-02"; "2017-01-02"; + "2018-01-02"; "2019-01-02"; "2023-01-02"; "2024-01-02"; "2029-01-02"; "2030-01-02"; + "2034-01-02"; "2035-01-02"; "2036-01-02"; "2040-01-02"; "2041-01-02"; "2045-01-02"; + "2046-01-02"; "2047-01-02" ] + (* [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). *) @@ -550,6 +566,11 @@ let layer_c_reason (l : row) (c : row) diffs = && subset diffs [ Season; Slug_f ] && String.equal c.slug "ef-time-after-epiphany-sunday-1" then Some "C15" + else if + List.mem l.date holy_name_fallback_dates_2005_2050 + && subset diffs [ Slug_f; Rank ] + && String.equal c.slug "ef-holy-name" + then Some "C16" else None (* ---------------------------------------------------------------------- *) diff --git a/test/test_golden.ml b/test/test_golden.ml index 1724375..0f90ee7 100644 --- a/test/test_golden.ml +++ b/test/test_golden.ml @@ -434,11 +434,21 @@ let test_ember_ferias_commemorated_when_impeded () = comms=[ef-advent-ember-fri:privileged] in=- out=[]"; (* 1902-02-22: Lent Ember Saturday (RG91 entry 18, II class) impeded by the Chair of St Peter (II class, universal, entry 16). Same RG24/ - RG109(e) mandate. *) - check ~msg:"1902-02-22: Lent Ember Saturday, impeded, MUST be commemorated (RG24) and is privileged (RG109e)" + RG109(e) mandate. ALSO, ef-holyname-rg110 task: RG 110's own + inseparable Peter/Paul commemoration -- "in Officio ... S. Petri semper + fit commemoratio S. Pauli" -- fires unconditionally whenever + [chair-of-st-peter] is observed, uncapped by RG 111's own admission + count, so `+paul` now rides alongside the Ember feria's own + commemoration rather than being displaced by it (precedence_ef.ml's own + [rg110_additions]). This pin's own [ef-lent-ember-sat:privileged] half + is UNCHANGED from before this task -- only [paul:ordinary] is new, + confirming RG 110 adds a genuinely SEPARATE, uncapped slot rather than + competing for the Ember feria's own privileged one. *) + check ~msg:"1902-02-22: Lent Ember Saturday, impeded, MUST be commemorated (RG24) and is privileged (RG109e) -- \ + AND RG110's Paul companion rides alongside it, uncapped" 1902 2 22 "1902-02-22 saturday season=lent week=1 slug=chair-of-st-peter rank=class-2 colour=white subject=saint \ - comms=[ef-lent-ember-sat:privileged] in=- out=[]" + comms=[ef-lent-ember-sat:privileged,paul:ordinary] in=- out=[]" let test_iv_class_ferias_never_commemorated () = (* 2026-07-22: an ordinary Time-after-Pentecost feria (Class4, RG91 entry @@ -634,6 +644,98 @@ let test_holy_family_earliest_possible_date_2024 () = "2024-01-07 sunday season=christmastide week=- slug=ef-time-after-epiphany-sunday-1 rank=class-2 \ colour=white subject=lord comms=[] in=- out=[]" +(* Most Holy Name of Jesus (RG 17(a), scan-verified, docs/research/rules- + register.md §6.2/§4): "festum Ss.mi Nominis Iesu, celebrandum dominica + quae occurrit a die 2 ad 5 ianuarii (secus die 2 ianuarii)" -- unlike Holy + Family (RG 17(b), immediately above), this window CAN be, and routinely + is, empty of a Sunday (3,619 of 8,417 years domain-wide) -- the + calendarium's own fallback, "vel, ea deficiente, die 2 ianuarii", is what + the SECOND pin below exercises. + + *** THIS FILE'S OWN VACUITY WARNING, APPLIED DIRECTLY *** + + [test_holy_name_sunday_shape_2026] pins a year where the Sunday shape + fires -- slug/rank/colour/season/comms here are ALL already what + [T.sunday_slug]'s Christmastide branch computed before this task's fix + (the identical accident Holy Family's own [describe] note above already + explains for its shape): an unnamed Sunday and Holy Name of Jesus share + every one of those fields on this date. Reverting the fix leaves this + pin GREEN except for [subject] -- the ONE field it actually tests, same + discipline as Holy Family's own "coordinator finding 4" fix. This pin + earns its place only by asserting [subject] explicitly, not by pinning + the whole line uncritically. + + [test_holy_name_fallback_2029] is the pin that actually has teeth against + the LARGER of the two defects this task closed: before the fix, colitur + emitted NO Holy Name office at all in a fallback year -- not merely an + unnamed generic Sunday (Holy Family's own pre-fix shape), but the + ordinary Christmastide ferial slug (`ef-christmas-1-tuesday`, class-4) + the task brief's own example names. Reverting the fallback half of the + fix reddens this pin on EVERY field (slug, rank, subject), not only one -- + the shape this file's header calls "the whole line moved", the opposite + of the Sunday pin's own narrow, single-field teeth. *) +let test_holy_name_sunday_shape_2026 () = + check ~msg:"2026-01-04: Holy Name of Jesus, Sunday shape -- oracle-corroborated (missalemeum's own \ + 2026-01-04 row: rank 2, white, title \"Holy Name of Jesus\")" + 2026 1 4 + "2026-01-04 sunday season=christmastide week=- slug=ef-holy-name-sunday rank=class-2 colour=white \ + subject=lord comms=[] in=- out=[]" + +let test_holy_name_fallback_2029 () = + check ~msg:"2029-01-02: Holy Name of Jesus, FALLBACK shape (RG 17(a)'s own \"secus die 2 ianuarii\", the \ + calendarium's \"vel, ea deficiente, die 2 ianuarii\") -- no Sunday falls 2-5 January 2029 \ + (`date -d 2029-01-0{2,3,4,5} +%u` = 2,3,4,5, independently confirmed), so 2 January itself \ + carries the feast; before this task's fix colitur emitted `ef-christmas-1-tuesday class-4` \ + here instead -- a real, II-class office simply missing, not merely misnamed" + 2029 1 2 + "2029-01-02 tuesday season=christmastide week=- slug=ef-holy-name rank=class-2 colour=white \ + subject=lord comms=[] in=- out=[]" + +(* RG 110 (docs/research/rules-register.md §4, Caput XIV), fix round 1 + (coordinator findings F1/F2): two golden pins neither existed before this + round -- F2 named 30 June's own total absence of golden coverage + directly ("no golden pin on any 30 June date"), and F1 named the + shape-(c) collision as a real day this suite otherwise never exercises + end-to-end against real data (both new [admit_cases] rows are synthetic + candidates, not `Calendar.day` over the real committed layer). + + *** BOTH PINS ARE BLIND TO COMMEMORATION ORDER -- STATED HONESTLY, NOT + CLAIMED AS PROOF OF F1's OWN FIX *** + + [describe]'s own `comms` field (this file's header, above) sorts the + commemoration list alphabetically before rendering + (`List.sort compare`) -- deliberately, so a golden pin's own STRING + comparison is not accidentally order-sensitive where nothing textual + requires it to be. That means this file, like the differential and the + oracle's own [identity_diff] (test_oracle.ml), CANNOT see whether + [rg110_additions] emits `chair-of-st-peter, paul` or `paul, + chair-of-st-peter` -- both sort to the same string. The ONLY place in + this codebase's test suite that asserts commemoration ORDER at all is + `test_precedence_ef.ml`'s own [admit_cases] table (`Alcotest.(check + (list string))`, not a sorted string) -- see precedence_ef.ml's own + [rg110_additions] comment for the citation this uses to fix the order, + and CLAUDE.md's "know what each layer cannot see" section for this as a + permanent, structural limit, not a gap this task's own pins happened not + to close. These two pins prove PRESENCE and IDENTITY of both + commemorations on a real, `Calendar.day`-resolved date -- genuine + ground no other pin in this file currently covers -- not order. *) +let test_rg110_june_30_2026 () = + check ~msg:"2026-06-30: RG 110's 30-June direction -- In Commemoratione Sancti Pauli Apostoli observed, \ + +commemoration-of-st-peter (data/ef/adjustments.sexp's own `Add` directive) the sole \ + commemoration -- missalemeum shows neither (M19, verdict colitur)" + 2026 6 30 + "2026-06-30 tuesday season=time-after-pentecost week=5 slug=in-commemoratione-sancti-pauli-apostoli \ + rank=class-3 colour=red subject=saint comms=[commemoration-of-st-peter:ordinary] in=- out=[]" + +let test_rg110_shape_c_2004 () = + check ~msg:"2004-02-22: RG 110 shape (c) -- Chair of St Peter itself loses the day to an ordinary \ + (Septuagesima, not I-class) II-class Sunday and is admitted only as a commemoration under \ + RG 111(b)'s own rank floor; Paul rides along too, uncapped (`date -d 2004-02-22 +%A` = \ + Sunday, independently confirmed)" + 2004 2 22 + "2004-02-22 sunday season=septuagesima week=3 slug=ef-septuagesima-sunday-3 rank=class-2 colour=violet \ + subject=temporal comms=[chair-of-st-peter:ordinary,paul:ordinary] in=- out=[]" + (* 2025-02-02, the other real instance of the SAME pattern -- not a contrasting second direction, per this task's fix round 1 (CRITICAL finding, item 1, reverted; see register §6.0 for the full account). @@ -793,6 +895,19 @@ let suite = `Quick test_holy_family_excludes_baptism_2030; Alcotest.test_case "Holy Family, earliest possible date (2024-01-07)" `Quick test_holy_family_earliest_possible_date_2024; + Alcotest.test_case "Holy Name of Jesus (RG17(a)), Sunday shape, subject asserted (2026-01-04)" `Quick + test_holy_name_sunday_shape_2026; + Alcotest.test_case + "Holy Name of Jesus, fallback shape (RG17(a) \"secus die 2 ianuarii\", 2029-01-02 -- no Sunday in \ + the window)" + `Quick test_holy_name_fallback_2029; + Alcotest.test_case + "RG110: 30 June, In Commemoratione S. Pauli Apostoli + commemoration-of-st-peter (2026-06-30)" `Quick + test_rg110_june_30_2026; + Alcotest.test_case + "RG110 shape (c): Chair of St Peter admitted only as a commemoration of an ordinary Sunday, Paul \ + rides along (2004-02-22)" + `Quick test_rg110_shape_c_2004; Alcotest.test_case "ef-rebootstrap: RG91 entry24 (III-class feast) outranks entry28 (class-4 feria) -- the six dates \ deferred since 2038's own Easter-extreme pin (2008-04-02/04/05, 2038-03-06/08/09)" diff --git a/test/test_oracle.ml b/test/test_oracle.ml index 35d4501..58ceb26 100644 --- a/test/test_oracle.ml +++ b/test/test_oracle.ml @@ -599,23 +599,23 @@ let m10_dates = [ "2026-11-30"; "2027-11-30"; "2026-12-08"; "2027-12-08" ] own full note and the task report. *) let m11_dates = [ "2026-12-26"; "2026-12-28"; "2027-12-27"; "2027-12-28" ] -(* M12 -- RG 110, primary text: "In Officio et Missa S. Petri semper fit - commemoratio S. Pauli, et vicissim" -- in the Office and Mass of EITHER - Peter or Paul, a commemoration of the OTHER is ALWAYS made (the - "inseparable commemoration"), uncapped by RG 111's ordinary admission - count ("in numero orationum computando, pro unica habeantur" -- counted - as one for counting purposes, i.e. it rides alongside whatever RG 111 - already admits). Unimplemented in this codebase -- confirmed by the one - row in this window where it is visible: 22 February 2027 (Chair of - St Peter) shows only the privileged Lent feria commemorated in colitur, - while missalemeum shows that AND "St. Paul". A genuine, primary-cited - feature gap -- verdict missalemeum, register §6 open item. Real but - NARROW: every other Peter/Paul office in this window (29 June, both - years; 1 August M4-adjacent dates were checked and found clean) either - had no room for the extra commemoration to become visible against this - comparator's axes, or fell on a day this predicate does not otherwise - touch -- see the task report for the specific check. *) -let m12_dates = [ "2027-02-22" ] +(* M12 -- CLOSED (ef-holyname-rg110 task): RG 110 is now built + (precedence_ef.ml's own [rg110_additions]/[rg110_companion_slug]), and + this window's one visible instance -- 22 February 2027 -- no longer + diverges from missalemeum on [Comm_count] at all: colitur now shows the + privileged Lent feria's own commemoration AND "St. Paul", same COUNT + (two) as missalemeum. Removed rather than kept as a dead 0-row citation, + the SAME shape M17's own removal already established (this file's own + header): the count pin now enforces that this exact divergence cannot + silently reappear. NOT closed with zero remaining difference, though -- + see M15's own widened date list immediately below: once count agrees, + IDENTITY comparison is reached for the first time on this date, and + trips a SEPARATE, pre-existing, unrelated comparator limit (one of + colitur's two admitted commemorations, [ef-lent-2-monday], is + TEMPORAL-origin and carries no English name) -- a newly-exposed + instance of M15's own gap, not a new one, folded into M15's date list + rather than kept here under RG 110's own citation, which no longer + explains ANY part of this row's remaining diff. *) (* M13 -- OPEN, NOT adjudicated (the brief's own explicit permission, "say so as an open item rather than absorbing it"). 19 March 2027: St @@ -647,13 +647,13 @@ let m13_dates = [ "2027-03-19" ] either engine is wrong about: a genuine LIMIT of this comparator, honestly counted rather than silently passed (the brief's own explicit instruction: "a day whose commemoration cannot be matched must be - visibly counted and reported, not quietly passed"). On each of these 19 + visibly counted and reported, not quietly passed"). On 19 of these 20 rows, colitur's own admitted commemoration is TEMPORAL-origin (an impeded Advent/Lent feria) -- Rite_ef.Temporal_ef's own [build] (temporal_ef.ml) never sets a celebration's [names] field, so there is no English string on colitur's side to compare against missalemeum's title text at all (this file's own header, "THE MAPPING, AND EXACTLY WHAT IT - CANNOT RESOLVE"). Every one of these 19 oracle titles reads as a plain + CANNOT RESOLVE"). Every one of those 19 oracle titles reads as a plain prose description of the SAME temporal ferial slot colitur independently computes for that date (e.g. 2 Dec 2026: oracle "Feria IV after I Sunday of Advent" against colitur's own [ef-advent-1-wednesday]; 4 Dec 2027: @@ -664,12 +664,25 @@ let m13_dates = [ "2027-03-19" ] unresolved. Fixing this at the ROOT (giving every temporal-cycle candidate an English name) is a data/lectionary-bootstrap task, not something this comparator can do for itself -- register §6, "RG 113 - tie-break" section's own neighbour, records it as an open item. *) + tie-break" section's own neighbour, records it as an open item. + + ADDED, ef-holyname-rg110 task: 2027-02-22 -- the closed M12's own date, + above. NOT the same shape as the other 19 (colitur admits TWO + commemorations there, not one -- [paul], sanctoral-origin and + resolvable, and [ef-lent-2-monday], temporal-origin and not): the + comparator's own [identity_diff] bails out to [Comm_identity_unresolved] + the moment ANY admitted commemoration is unresolvable, regardless of how + many others resolve cleanly (test_oracle.ml's own [identity_diff], + "[List.exists] ... name = None"), so this row reaches the identical + diagnosis for the identical structural reason, once RG 110 stopped + [Comm_count] from masking it. Genuinely newly-EXPOSED by this task's own + fix, not newly-CAUSED by it: the underlying gap (no English name for a + temporal-cycle candidate) is exactly M15's own pre-existing one. *) let m15_dates = [ "2026-02-24"; "2026-03-19"; "2026-03-25"; "2026-12-02"; "2026-12-03"; "2026-12-04"; - "2026-12-07"; "2026-12-11"; "2026-12-21"; "2027-02-24"; "2027-12-02"; "2027-12-03"; - "2027-12-04"; "2027-12-06"; "2027-12-07"; "2027-12-11"; "2027-12-13"; "2027-12-16"; - "2027-12-21" ] + "2026-12-07"; "2026-12-11"; "2026-12-21"; "2027-02-22"; "2027-02-24"; "2027-12-02"; + "2027-12-03"; "2027-12-04"; "2027-12-06"; "2027-12-07"; "2027-12-11"; "2027-12-13"; + "2027-12-16"; "2027-12-21" ] (* M16 -- Task B: a genuine IDENTITY mismatch, both sides otherwise agreeing on rank/colour/count/presence. 27 March 2026 (Friday of Passion Week): @@ -766,6 +779,49 @@ let m16_dates = [ "2026-03-27" ] 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. *) +(* M19 -- ef-holyname-rg110 task: RG 110's OTHER direction, 30 June (this + window's two instances -- both years -- of + [in-commemoratione-sancti-pauli-apostoli], data/ef/adjustments.sexp's + own `Add commemoration-of-st-peter` directive; see that file's own + citation for the full scan quote). Unlike 22 February above, + missalemeum's own row for both dates shows an EMPTY commemorations array + ("St. Peter" absent, and not even listed as "displaced" -- nothing + competes for the slot on either side, so there is no candidate for + missalemeum's own engine to have rejected, only one it never + constructed): missalemeum has the SAME gap this task's own scan found in + lectio's source data (`~/git/projects/lectio/internal/caldata/ + tridentine-calendar.ini` has no 06-30 companion entry either) -- a + genuine feature gap, verdict colitur, the SAME shape as M1/M3/M8/M10 + above (each already "missalemeum does not implement X"), not a + citation-count coincidence: RG 110's own text is unconditional ("in + Officio et Missa S. Petri semper fit commemoratio S. Pauli, ET + VICISSIM"), and the calendarium's own June table states the SAME + pattern already built for the other two pairs, word for word. + + IDENTITY-GATED, fix round 1 (coordinator finding F2): the predicate + below now also requires [c.c_commemorations] to be EXACTLY the single + entry [commemoration-of-st-peter] -- not merely that some [Comm_presence] + diff exists on these two dates. Proved necessary, not decorative: the + reviewer added a SECOND, fabricated `Add` directive on 30 June (a + `bogus-fabricated-companion` slug) to a scratch copy of + data/ef/adjustments.sexp and found the whole suite stayed green with the + pre-fix, presence-only predicate -- the exact C6/C14 failure mode (a + Layer-C-style predicate that pins a date and a diff SHAPE but not WHICH + candidate). This matters more here than for any other entry in this + file: `commemoration-of-st-peter` is corroborated by NEITHER oracle (it + rests on a scan reading alone, data/ef/adjustments.sexp's own citation), + so this allow-list predicate is the only place in the whole suite that + could assert what colitur actually emits here -- and, pre-fix, asserted + nothing about it. Mirrors C16's own identity guard + (`String.equal c.slug "ef-holy-name"`, test_differential.ml) at the + analogous decision point in this file. *) +let m19_dates = [ "2026-06-30"; "2027-06-30" ] + +let m19_commemoration_matches (c : colitur_row) = + match c.c_commemorations with + | [ (slug, _, _, _) ] -> String.equal slug "commemoration-of-st-peter" + | _ -> false + let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = if diffs = [] then None (* M1's own subset widened (this task): colitur's observed day on both @@ -795,7 +851,6 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = 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 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 @@ -818,6 +873,8 @@ let layer_m_reason (c : colitur_row) (o : oracle_row) diffs = && subset diffs [ Comm_identity_mismatch; Observed_identity_unresolved ] then Some "M16" else if diffs = [ Observed_identity_unresolved ] then Some "M18" + else if List.mem c.c_date m19_dates && diffs = [ Comm_presence ] && m19_commemoration_matches c then + Some "M19" else None (* ---------------------------------------------------------------------- *) diff --git a/test/test_precedence_ef.ml b/test/test_precedence_ef.ml index 4ee2fd8..ed9a933 100644 --- a/test/test_precedence_ef.ml +++ b/test/test_precedence_ef.ml @@ -1016,6 +1016,43 @@ let sixtus = UNIVERSALLY), so the two values coinciding is the faithful shape, not a simplification that dodges the new parameter. The last two rows are where they genuinely differ -- see their own comment. *) +(* RG 110 (§4, Caput XIV): candidates built with the REAL trigger/companion + slugs {!PE.rg110_companion_slug} matches on -- unlike every other + [admit_cases] candidate above, which is deliberately built with a + SYNTHETIC slug {!PE.band}'s 28 branches (and this file's own RG 110 + lookup) cannot recognise, these three rows need [PE.admit] to recognise + the slug itself, so the real ones are used directly. [~status: + Cel.Commemoration_only] on the companions matches the real data shape + (data/ef/sanctoral.sexp's own [peter]/[paul]; + data/ef/adjustments.sexp's own [commemoration-of-st-peter]) even though + [admit] itself never reads [status] -- only [Precedence.resolve]'s own + partition does, upstream of [admit] -- kept for a reader's sake, not a + test requirement. *) +let chair_of_st_peter_synth = cand ~rank:V.Class2 "chair-of-st-peter" +let paul_companion_synth = cand ~rank:V.Class3 ~status:Cel.Commemoration_only "paul" + +(* An unrelated third commemoration, sorting AFTER both members of the RG110 + pair, so that "spliced immediately after its trigger" and "appended at the + end" give different answers. Added by the fix-round re-review (G1): with + only the pair present the two are indistinguishable, and the re-review + proved an append-at-end variant passed the whole suite green. Class3 + Commemoration_only so it cannot win the day; its slug is deliberately not + one {!PE.rg110_companion_slug} recognises. *) +let unrelated_comm_synth = + cand ~rank:V.Class3 ~status:Cel.Commemoration_only "zzz-unrelated-commemoration" + +(* Any real Lent/Ember feria's own shape (a Class3, PRIVILEGED, temporal- + origin candidate) -- {!PE.rg110_companion_slug} does not match this slug, + so it is a neutral bystander in [admit]'s own normal (pre-RG110) contest, + exactly like [privileged_lo] elsewhere in this table, just kept + separately named so this block's own rows are self-contained and do not + depend on a binding declared for an unrelated purpose above. Given the + WORSE (higher) precedence int than [paul_companion_synth] would need to + win the day's one "other II class" slot on its own dignity ALONE -- the + whole point of the row below is that RG 110 does not care who wins that + contest; it adds Paul regardless. *) +let lent_feria_synth = (cand ~origin:P.Temporal ~rank:V.Class3 "ef-lent-2-monday-synth", P.Privileged, 180) + let admit_cases = [ (* RG 111 (§4): "I class: none save one privileged." *) ( "I-class day, only an ordinary commemoration due -> none admitted", @@ -1109,7 +1146,98 @@ let admit_cases = admitted", lord_winner, lord_winner, [ po sixtus P.Ordinary ], - [ "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" ] ) + [ "pope-sixtus-ii-felicissimus-and-agapitus-martyrs" ] ); + (* RG 110, shape (a)/(b): [chair-of-st-peter] observed, a competing + PRIVILEGED Lent feria also due for the day's single "other II class" + slot (RG 111(c) itself has no privilege-override clause -- unlike the + Sunday branch above -- so pure precedence order decides that slot on + its own, and [lent_feria_synth]'s own 180 beats + [paul_companion_synth]'s 999). Without RG 110, this is exactly + precedence_ef.ml's own measured defect (3,533 of 8,417 domain-wide + years, this task's own report): Paul loses the slot outright and is + never admitted at all. With RG 110, Paul is added ADDITIONALLY, + PREPENDED ahead of the feria -- RG 110(a)/(b)'s own text ("oratio + alterius Apostoli additur ... orationi diei", the companion's oration + added to the DAY's own oration) governs here, not (c)'s "ante omnes + alias commemorationes" (CORRECTED, fix round 1, coordinator finding + F1: this comment previously cited (c) for both shapes uniformly -- + see [rg110_additions]'s own comment in precedence_ef.ml for why the + two shapes need, and get, DIFFERENT orderings, not the same one for + different reasons) -- both admitted, not one displacing the other. *) + ( "RG110 (a)/(b): chair-of-st-peter observed, Paul rides alongside a \ + competing privileged Lent feria, uncapped and prepended", + chair_of_st_peter_synth, chair_of_st_peter_synth, + [ (paul_companion_synth, P.Ordinary, 999); lent_feria_synth ], + [ "paul"; "ef-lent-2-monday-synth" ] ); + (* RG 110, shape (c): [chair-of-st-peter] itself LOSES the day outright + (observed is some unrelated II-class Sunday) but is admitted, on its + OWN Class2 dignity, as RG 111(b)'s single ordinary commemoration -- + exactly the domain-wide 593-year pattern this task's own report + measures. RG 110(c)'s own text, word for word ("quoties vero oratio + unius Apostoli addenda est ad modum commemorationis, HUIC ORATIONI + ADDITUR ALTERA immediate, ante omnes alias commemorationes"): the + companion (Paul, Class3, no standing of its OWN for this Sunday's + rank-floored slot) rides along anyway, uncapped -- ADDED TO THE + TRIGGER'S own oration ("huic orationi additur altera"), i.e. + FOLLOWING it, not preceding it. CORRECTED, fix round 1 (coordinator + finding F1): this row originally asserted `["paul"; + "chair-of-st-peter"]` -- the companion BEFORE the trigger -- citing + the SAME "ante omnes alias commemorationes" clause that actually + governs the PAIR's position relative to OTHER, unrelated + commemorations, not the pair's own internal order. "Huic orationi" + (dative, "to THIS oration") refers back to "oratio unius Apostoli", + the trigger's own oration -- so the companion is added TO it, + following, not leading. Reproduced by mutation: reverting + [rg110_additions]'s shape-(c) branch back to a uniform prepend + reddens exactly this row's own expected order, nothing else. *) + ( "RG110 (c): chair-of-st-peter admitted merely AS a commemoration \ + (loses the day to an ordinary II-class Sunday), Paul rides along \ + AFTER it (RG110(c): \"huic orationi additur altera\")", + cand ~rank:V.Class2 "ef-some-other-sunday", cand ~rank:V.Class2 "ef-some-other-sunday", + (* A third, unrelated candidate is OFFERED here deliberately, and is + correctly NOT admitted -- which is itself the point. + + The fix-round re-review asked for a third commemoration so that + "spliced IMMEDIATELY after its own trigger" (RG110(c)'s own word) + could be told apart from "appended at the end of the list": with a + single-member result the two coincide, and it proved an + append-at-end variant passed the whole suite green. + + Attempting it establishes something better than the test it asked + for: on this day-shape a third commemoration is RUBRICALLY + IMPOSSIBLE, so the distinction is unobservable BY CONSTRUCTION, not + merely unobserved. Shape (c) requires the trigger to LOSE the day + while still being admitted -- and a Class2 trigger can only lose to + a I-class day (RG 111(a): one privileged commemoration) or a + II-class Sunday (RG 111(b): one, and "scilicet de festo II + classis"). Both caps are ONE. Anything a third candidate could be + is either outranked by the trigger or excluded by the rank floor; + here [zzz-unrelated-commemoration] is Class3 and RG 111(b) drops + it, leaving the pair alone. + That is why the re-review's own domain sweep found shape (c) never + co-occurring with a third commemoration in 8 417 years: not an + accident of the data, a consequence of the rubric. + + So RG110(c)'s "ante omnes alias commemorationes" half has no + reachable witness in the EF's real rubrical space, and the + adjacency this row asserts is exercised only against the pair + itself. The append-at-end variant therefore remains + indistinguishable from the splice on any input the engine can + actually construct -- recorded in the register rather than papered + over with a synthetic row that proves a shape no rubric permits. *) + [ po chair_of_st_peter_synth P.Ordinary; + (paul_companion_synth, P.Ordinary, 999); + po unrelated_comm_synth P.Ordinary ], + [ "chair-of-st-peter"; "paul" ] ); + (* Defensive: a trigger slug is [observed], but its companion is simply + not present among [comms] at all (a data gap, or -- as this exact + shape was for 30 June before this task's own [Add + commemoration-of-st-peter] directive -- an upstream omission). + {!PE.rg110_find_companion} must return [None] gracefully, not raise; + the day's own (empty, here) normal result is unaffected. *) + ( "RG110 defensive: chair-of-st-peter observed, no companion present in \ + comms at all -> no crash, nothing added", + chair_of_st_peter_synth, chair_of_st_peter_synth, [], [] ) ] (* Order independence (brief: "the admitted set must not depend on input @@ -1426,6 +1554,103 @@ let test_class1_feast_inside_holy_familys_window_end_to_end () = Alcotest.failf "expected exactly one Privileged commemoration (Holy Family), got %d" (List.length other)) +(* Most Holy Name of Jesus (RG 17(a)) -- the SAME two proofs immediately + above, restated for Holy Name instead of Holy Family. UNLIKE Holy Family, + there is no LIVE collision to exercise here (this file's own [cases]/ + [disposition_cases] rows never construct one, because + data/ef/sanctoral.sexp has nothing fixed-date 2-5 January besides + [telesphorus-pope-and-martyr] (5 Jan, Commemoration_only, so it can never + contest [observed] at all -- {!P.resolve}'s own partition holds it out of + the band contest entirely) -- register §6.2's own account: "no live + collision exists for it in this codebase's current data". These two + tests are the SYNTHETIC proof the task brief asked for instead: that + {!band}'s [entry_14_movable_band]/RG 112(a) mechanism, ALREADY proved + above for Holy Family, governs Holy Name identically, since neither + {!band} nor {!disposition} key on anything Holy-Family-specific (only on + [subject]/[rank]/[origin], which [temporal_ef.ml]'s fix now sets + identically for both feasts) -- not a live-data regression test, a + mechanism-generalises-correctly one, the same distinction this file's own + header draws between a table row and an end-to-end proof. + + Run against BOTH of RG 17(a)'s own shapes -- the Sunday (2026-01-04) and + the 2 January fallback (2029-01-02, {!T.holy_name_fallback_date}) -- + because [temporal_ef.ml]'s own fallback-branch comment claims both band + identically at {!PE.entry_14_movable_band} (ONE feast, not two + differently-classified ones); a table proving the SAME two outcomes hold + for both dates is direct evidence for that specific claim, not merely + for the Sunday shape colitur already had a name for before this task. *) +let holy_name_probe_dates = + [ ("Sunday shape (2026-01-04)", mk 2026 1 4); ("fallback shape (2029-01-02)", T.holy_name_fallback_date 2029) ] + +let test_holy_name_excludes_synthetic_lord_end_to_end () = + List.iter + (fun (label, date) -> + let holy_name = of_temporal date in + Alcotest.(check bool) (label ^ ": sanity -- temporal_ef really did tag this Lord") true + (holy_name.P.cel.Cel.subject = Sub.Lord); + let day_ctx = ctx date in + let synthetic_lord = + cand ~origin:P.Sanctoral ~rank:V.Class2 ~subject:Sub.Lord ~layer:PE.universal_layer + "ef-synthetic-fixed-lord-probe" + in + let resolution = P.resolve real_rules day_ctx ~temporal:holy_name ~sanctoral:[ synthetic_lord ] in + Alcotest.(check string) (label ^ ": Holy Name is observed (RG91 e14: movable first)") + (S.to_string holy_name.P.cel.Cel.slug) (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check int) (label ^ ": no commemorations at all -- RG112(a)'s own exclusion") 0 + (List.length resolution.P.commemorations); + Alcotest.(check (list string)) (label ^ ": the synthetic Lord candidate is OMITTED, not commemorated") + [ "ef-synthetic-fixed-lord-probe" ] + (List.map (fun (c, _) -> S.to_string c.P.cel.Cel.slug) resolution.P.omitted)) + holy_name_probe_dates + +(* A REAL asymmetry this test found between the two shapes, kept explicit + rather than papered over by forcing one uniform expectation on both -- + RG 109(a)'s privilege is textually "of a SUNDAY" (docs/research/rules- + register.md §4), read here via {!PE.is_sunday_slug}'s own substring test: + the Sunday shape's slug ("ef-holy-name-sunday") carries the marker and + the fallback shape's ("ef-holy-name") genuinely does not, because 2 + January genuinely is not a Sunday. So a losing Holy-Name-Sunday is + PRIVILEGED (RG109(a)) and survives RG111(a)'s "I class: none save one + privileged" cap when it loses to a higher day; a losing Holy-Name- + fallback has no RG109(a)-(f) category to claim (not a Sunday, not I + class, none of (c)-(f) either) and so is only ORDINARY -- which + RG111(a)'s own cap admits NONE of on a I-class day. Both outcomes are + individually correct readings of RG 109/111's own closed lists, not a + defect in either shape -- first observed as a test failure while writing + this pair (the original version of this test asserted one Privileged + commemoration for BOTH shapes uniformly and reddened here), traced to + this citation rather than forced to pass. *) +let test_class1_feast_inside_holy_name_window_end_to_end () = + List.iter + (fun (label, date, expect_privileged_commemoration) -> + let holy_name = of_temporal date in + let day_ctx = ctx date in + let synthetic_class1 = + cand ~origin:P.Sanctoral ~subject:Sub.Saint ~layer:PE.universal_layer "ef-synthetic-class1-probe-hn" + in + let resolution = P.resolve real_rules day_ctx ~temporal:holy_name ~sanctoral:[ synthetic_class1 ] in + Alcotest.(check string) (label ^ ": the I-class candidate is observed, NOT Holy Name (RG91 e11 beats e14-movable)") + "ef-synthetic-class1-probe-hn" (S.to_string resolution.P.observed.P.cel.Cel.slug); + Alcotest.(check (list string)) (label ^ ": nothing transfers") [] + (List.map (fun c -> S.to_string c.P.cel.Cel.slug) resolution.P.deferred); + if expect_privileged_commemoration then + match resolution.P.commemorations with + | [ (c, P.Privileged) ] -> + Alcotest.(check string) + (label ^ ": Holy Name itself is privileged-commemorated (RG109(a)), neither omitted nor transferred") + (S.to_string holy_name.P.cel.Cel.slug) (S.to_string c.P.cel.Cel.slug) + | other -> + Alcotest.failf "%s: expected exactly one Privileged commemoration (Holy Name), got %d" label + (List.length other) + else + Alcotest.(check int) + (label ^ ": RG111(a) admits none -- Holy Name's own commemoration here is Ordinary \ + (no RG109(a)-(f) category applies to a non-Sunday loser), and a I-class day admits \ + only a Privileged one") + 0 (List.length resolution.P.commemorations)) + [ ("Sunday shape (2026-01-04)", mk 2026 1 4, true); + ("fallback shape (2029-01-02)", T.holy_name_fallback_date 2029, false) ] + (* Completes Task 7's carried fix (RG 91 entry 8, §4): on a real Sunday landing on 2 November, All Souls does not merely lose (that was Task 7's [band] fix, proved by [test_all_souls_yields_to_sunday] above) -- it must @@ -1681,6 +1906,14 @@ let suite = "fix round 1, finding 1: a synthetic I-class candidate inside Holy Family's own window is \ observed and does not transfer (2026-01-11)" `Quick test_class1_feast_inside_holy_familys_window_end_to_end; + Alcotest.test_case + "RG112(a) + RG91 e14 \"primum mobilia\": Holy Name of Jesus excludes a synthetic fixed Lord \ + candidate entirely, both shapes (Sunday + fallback)" + `Quick test_holy_name_excludes_synthetic_lord_end_to_end; + Alcotest.test_case + "a synthetic I-class candidate inside Holy Name's own window is observed and does not \ + transfer, both shapes (Sunday + fallback)" + `Quick test_class1_feast_inside_holy_name_window_end_to_end; Alcotest.test_case "transfer_target: general RG96 search walks past more than one blocked day" `Quick test_transfer_target_general_multi_step_search; Alcotest.test_case "transfer_target: Annunciation exception starts at Monday after Low Sunday" diff --git a/test/test_temporal_ef.ml b/test/test_temporal_ef.ml index 23a817d..f341533 100644 --- a/test/test_temporal_ef.ml +++ b/test/test_temporal_ef.ml @@ -272,6 +272,105 @@ let test_holy_family_no_gap_year () = Alcotest.(check string) "2025 (6 Jan a Monday): 12 Jan" "2025-01-12" (D.to_iso8601 (T.holy_family_sunday 2025)) +(* RG 17(a) (Caput III, "De Dominicis"; full text on [T.holy_name_sunday]'s + own citation), Sunday shape: "festum Ss.mi Nominis Iesu, celebrandum + dominica quae occurrit a die 2 ad 5 ianuarii". Deliberately NOT part of + [T.named], same reason as Holy Family (that test's own comment): the + slug/rank/colour this branch produces for a plain Sunday 2-5 January + already match what [T.sunday_slug]'s Christmastide branch always + computed; only [subject]/[names] change. *) +let test_holy_name_sunday_shape () = + (* 2026: 4 January is the Sunday (independently cross-checked against + `date -d 2026-01-04 +%u` = 7). Oracle-corroborated: missalemeum's own + 2026-01-04 row is rank 2, white, title "Holy Name of Jesus" (the task + brief's own example). *) + Alcotest.(check string) "2026: slug is the ordinary Holy-Name-window Sunday key" "ef-holy-name-sunday" + (slug_of (d 2026 1 4)); + Alcotest.(check string) "2026: II class" "class-2" (rank_of (d 2026 1 4)); + Alcotest.(check string) "2026: white" "white" (colour_of (d 2026 1 4)); + Alcotest.(check bool) "2026: subject is Lord (RG 91 entry 14, \"Festa Domini II classis\")" true + (subject_of (d 2026 1 4) = Sub.Lord); + Alcotest.(check (option string)) "2026: Latin name from the calendarium/propers, verbatim" + (Some "Sanctissimi Nominis Iesu") (name_la_of (d 2026 1 4)); + Alcotest.(check (option string)) "2026: no English name (deliberate -- would read the oracle it is \ + compared against)" + None (Colitur_kernel.Names.find (office (d 2026 1 4)).Cel.names en); + (* Negative controls: every OTHER day in the 2-5 January window that year + stays [subject Temporal] -- the override is scoped to exactly the one + Sunday [T.holy_name_sunday] names, not the whole window. *) + List.iter + (fun dd -> + Alcotest.(check bool) (Printf.sprintf "2026-01-%02d: NOT Lord (not the Sunday)" dd) true + (subject_of (d 2026 1 dd) = Sub.Temporal)) + [ 2; 3; 5 ]; + (* 2027: 3 January is the Sunday (`date -d 2027-01-03 +%u` = 7) -- the + oracle window's own second instance of the Sunday shape. missalemeum's + 2027-01-03 row independently corroborates: rank 2, white, title "Holy + Name of Jesus". *) + Alcotest.(check bool) "2027: 3 January is a Sunday" true (D.weekday (d 2027 1 3) = D.Sun); + Alcotest.(check string) "2027: same slug" "ef-holy-name-sunday" (slug_of (d 2027 1 3)); + Alcotest.(check bool) "2027: still subject Lord" true (subject_of (d 2027 1 3) = Sub.Lord); + (* Cross-checked against [T.holy_name_sunday] itself, independent of the + literal dates above. *) + Alcotest.(check (option string)) "2026: T.holy_name_sunday agrees" (Some "2026-01-04") + (Option.map D.to_iso8601 (T.holy_name_sunday 2026)); + Alcotest.(check (option string)) "2027: T.holy_name_sunday agrees" (Some "2027-01-03") + (Option.map D.to_iso8601 (T.holy_name_sunday 2027)) + +(* RG 17(a)'s own fallback clause, "(secus die 2 ianuarii)" -- the calendarium + confirms it in its own words: "vel, ea deficiente, die 2 ianuarii: + Sanctissimi Nominis Iesu, II classis". Unlike Holy Family's 7-13 January + window, this narrower 2-5 January window CAN be empty of a Sunday -- + 3,619 of the 8,417 years in [1583, 9999] -- and colitur emitted NO Holy + Name office at all on any of them before this fix (register §6.2's own + account of the gap this closes). *) +let test_holy_name_fallback_shape () = + (* 2029: no Sunday falls 2-5 January (independently cross-checked: `date -d + 2029-01-0{2,3,4,5} +%u` are 2,3,4,5 -- Tuesday through Friday, none a + 7). The task brief's own reported example: "2029-01-02 + ef-christmas-1-tuesday class-4 white (no Sunday in window: feast + MISSING)" -- that is the pre-fix behaviour this test pins the fix + against. *) + Alcotest.(check (option string)) "2029: no Sunday in the window" None + (Option.map D.to_iso8601 (T.holy_name_sunday 2029)); + Alcotest.(check string) "2029: slug is the fallback key" "ef-holy-name" (slug_of (d 2029 1 2)); + Alcotest.(check string) "2029: II class" "class-2" (rank_of (d 2029 1 2)); + Alcotest.(check string) "2029: white" "white" (colour_of (d 2029 1 2)); + Alcotest.(check bool) "2029: subject is Lord, same as the Sunday shape" true + (subject_of (d 2029 1 2) = Sub.Lord); + Alcotest.(check (option string)) "2029: SAME Latin name as the Sunday shape (one feast, two dates)" + (Some "Sanctissimi Nominis Iesu") (name_la_of (d 2029 1 2)); + (* Negative controls: 3, 4 and 5 January stay ordinary Christmastide + ferias in a fallback year -- the fallback names 2 January specifically, + not "some day near the start of the window". *) + List.iter + (fun dd -> + Alcotest.(check bool) (Printf.sprintf "2029-01-%02d: NOT the feast (ordinary feria)" dd) true + (subject_of (d 2029 1 dd) = Sub.Temporal + && Sl.to_string (office (d 2029 1 dd)).Cel.slug <> "ef-holy-name")) + [ 3; 4; 5 ]; + (* A second, independently cross-checked fallback year, at the OTHER end + of the week (2008: `date -d 2008-01-02 +%A` = Wednesday), so this is + not a single-year accident. *) + Alcotest.(check (option string)) "2008: no Sunday in the window either" None + (Option.map D.to_iso8601 (T.holy_name_sunday 2008)); + Alcotest.(check string) "2008: same fallback slug" "ef-holy-name" (slug_of (d 2008 1 2)) + +(* [T.holy_name_sunday]'s own [Some]/[None] split, cross-checked against + `date -d` independently for a sample spanning both shapes -- not merely + the two years the shape tests above already pin literally, so the + boundary itself (not only two hand-picked instances) is exercised. *) +let test_holy_name_sunday_or_none () = + List.iter + (fun (y, expected) -> + Alcotest.(check (option string)) + (Printf.sprintf "%d: holy_name_sunday" y) + expected + (Option.map D.to_iso8601 (T.holy_name_sunday y))) + [ (2026, Some "2026-01-04"); (2027, Some "2027-01-03"); (2009, Some "2009-01-04"); + (2010, Some "2010-01-03"); (2020, Some "2020-01-05"); (2025, Some "2025-01-05"); + (2006, None); (2007, None); (2008, None); (2018, None); (2029, None) ] + (* Regression for a Task 14 Validate finding, reworked (register finding 4): [named] no longer carries its own week at all -- [temporal] computes it uniformly via [week] for every day, named or not -- so "a named day inside @@ -583,6 +682,56 @@ let test_holy_family_anchor_present_and_erosion_is_caught () = Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion" expected_slug (slug_of expected_date) +(* Holy Name of Jesus (RG 17(a)), SUNDAY shape -- same structure as Holy + Family's own test immediately above, same reasoning (also not one of + [T.named]'s outputs). 2026 is a Sunday-shape year (4 January). *) +let test_holy_name_sunday_anchor_present_and_erosion_is_caught () = + let y = 2026 in + let expected_date = + match T.holy_name_sunday y with + | Some d -> d + | None -> Alcotest.fail "sanity: 2026 must have a Sunday in the Holy Name window" + in + let expected_slug = + match T.sunday_slug expected_date with + | Some s -> s + | None -> Alcotest.fail "sanity: holy_name_sunday must itself be a Sunday" + in + Alcotest.(check (list string)) + "the anchor entry exists: T.sunday_slug's own independent computation is not missing from T.anchors" [] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:(anchor_slugs y)); + let eroded_anchors = List.filter (fun s -> s <> expected_slug) (anchor_slugs y) in + Alcotest.(check (list string)) "the erosion is caught: missing_from_anchors reports it, and only it" + [ expected_slug ] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:eroded_anchors); + Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion" + expected_slug (slug_of expected_date) + +(* Holy Name of Jesus, FALLBACK shape -- the SAME two properties (presence, + erosion caught), for a year with no Sunday in the window. Unlike the + Sunday shape above, there is no SECOND independent function (the way + [T.sunday_slug] is independent of [T.anchors]) that names "ef-holy-name" + for a non-Sunday date -- [expected_slug] is the literal this task chose, + cross-checked instead against [T.temporal]'s own output (the function + under test) via the final sanity line, same as every other branch of + this cross-check, honestly not a second independent source. 2029 has no + Sunday 2-5 January (test_holy_name_fallback_shape's own citation). *) +let test_holy_name_fallback_anchor_present_and_erosion_is_caught () = + let y = 2029 in + (match T.holy_name_sunday y with + | None -> () + | Some _ -> Alcotest.fail "sanity: 2029 must have no Sunday in the Holy Name window"); + let expected_date = T.holy_name_fallback_date y in + let expected_slug = "ef-holy-name" in + Alcotest.(check (list string)) "the anchor entry exists for the fallback shape" [] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:(anchor_slugs y)); + let eroded_anchors = List.filter (fun s -> s <> expected_slug) (anchor_slugs y) in + Alcotest.(check (list string)) "the erosion is caught: missing_from_anchors reports it, and only it" + [ expected_slug ] + (missing_from_anchors ~named_slugs:[ expected_slug ] ~anchors:eroded_anchors); + Alcotest.(check string) "sanity: temporal itself is unaffected by the anchors-list erosion" + expected_slug (slug_of expected_date) + let test_totality () = (* Every day of 2026 yields an office without raising. Not a slug re-validation -- Slug.t is a private string validated on construction, @@ -606,6 +755,9 @@ let suite_extra = Alcotest.test_case "sunday slugs" `Quick test_sunday_slugs; Alcotest.test_case "holy family (RG 17(b))" `Quick test_holy_family; Alcotest.test_case "holy family: no gap year (RG 17(b))" `Quick test_holy_family_no_gap_year; + Alcotest.test_case "holy name: Sunday shape (RG 17(a))" `Quick test_holy_name_sunday_shape; + Alcotest.test_case "holy name: fallback shape (RG 17(a))" `Quick test_holy_name_fallback_shape; + Alcotest.test_case "holy name: Sunday-or-none cross-check" `Quick test_holy_name_sunday_or_none; Alcotest.test_case "week/sunday_slug agree" `Quick test_week_sunday_slug_agree; Alcotest.test_case "resumed sundays" `Quick test_resumed_sundays; Alcotest.test_case "ferial slugs and ranks" `Quick test_ferial_slugs_and_ranks; @@ -620,6 +772,10 @@ let suite_extra = test_anchors_cover_all_named_days; Alcotest.test_case "holy family anchor present, erosion caught" `Quick test_holy_family_anchor_present_and_erosion_is_caught; + Alcotest.test_case "holy name (Sunday shape) anchor present, erosion caught" `Quick + test_holy_name_sunday_anchor_present_and_erosion_is_caught; + Alcotest.test_case "holy name (fallback shape) anchor present, erosion caught" `Quick + test_holy_name_fallback_anchor_present_and_erosion_is_caught; Alcotest.test_case "anchors erosion is caught" `Quick test_anchors_erosion_is_caught ] let suite = |
