aboutsummaryrefslogtreecommitdiff
path: root/lib/rites/rite_ef/precedence_ef.ml
blob: 39251a77a5ac56b8df8b8f50167a61695231b5ef (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
(* RG 91's Table of Precedence (docs/research/rules-register.md §4). Each
   branch below is one of the table's 28 entries, checked in the table's own
   numeric order -- lower wins, and because occasional entries are true
   exceptions to a later, broader one (RG 91 entry 18's Ember days are an
   exception carved out of entry 22's Lent ferias; entry 21/26's vigils are
   an exception carved out of the generic Class2/Class3 sanctoral-feast
   entries that would otherwise also match), checking in table order and
   returning on the first match is what makes the exception actually win
   without a separate exclusion for every later entry it pre-empts.

   Two kinds of evidence decide an entry:
   - The temporal cycle's own office (Nativity, a Sunday, a feria, All Souls)
     is identified structurally, from the context's date/season/weekday and
     the day's Easter offset -- never from its slug, which is just a label.
     [origin = Temporal] gates every such entry so a sanctoral candidate that
     happens to share a date (Immaculate Conception can never coincide with
     the movable cycle, but nothing stops a future rite bug from producing
     one) cannot be mistaken for the office itself. All Souls (entry 8, the
     one non-temporal-origin member of this group) additionally reads the
     context's weekday for its own register-stated exception -- see entry 8
     below.
   - A sanctoral feast's entry is decided by its [rank], and -- except at
     entry 14 (see its own comment below, where the register draws no such
     line) -- per the brief's structural insight, also by its
     {!Celebration.t}.layer: a celebration whose layer is not the universal
     base is an overlay, hence "proper" or "indult" rather than the
     universal entry (11-13 I class; 16/19/20 II class; 23/24 III class; see
     precedence_ef.mli). [origin = Sanctoral] gates these for the same
     reason: temporal-origin celebrations carry the literal layer id
     "temporal" (rite_ef/temporal_ef.ml's [build]), which is not
     [universal_layer] either, and would otherwise be misread as "proper" by
     the layer test alone.

   Vigils (21, 26) are the one shape neither of those two kinds fully
   describes on their own: a II/III-class vigil can be temporal-origin (the
   Ascension Vigil, produced by temporal_ef today) or sanctoral-origin (a
   saint's vigil, not yet loaded by any task), so its entry cannot be gated
   on [origin] at all. Nothing in the day's other fields marks "this is a
   vigil, not an ordinary office of the same rank" either, so this reads it
   off the temporal cycle's own slug convention (a "-vigil" suffix -- see
   [named] in temporal_ef.ml) rather than guessing a new one. *)

open Colitur_kernel

(* Not an RG citation -- RG 91 ranks proper and indult feasts, it does not
   encode how a computer tells them apart. See precedence_ef.mli. *)
let universal_layer = "ef-universal"
let indult_prefix = "indult:"
let unclassified = max_int

(* CORRECTED, fix round 1 (coordinator finding 1) -- the ORIGINAL version of
   this constant was [-14], justified only by "cannot collide with a real
   table position (1..28) or [unclassified]". That is a COLLISION argument,
   not an ORDERING one, and [band]'s own return value is compared by
   {!Precedence.compare_by}/{!compare_precedence} as a plain [<] -- a
   negative value is not merely "distinct from 1..28", it is LOWER than
   every one of them, so a movable Class2 Lord feast (Holy Family) would
   have beaten every I-class day (entries 1-13) outright, not merely the
   fixed Baptism it was built to beat. Not live against the shipped
   UNIVERSAL sanctoral data (no I-class universal feast falls 7-13 January),
   which is why the original 1583-9999 blast radius (§6.2) showed only the
   one intended shape -- but entries 12/13 admit PROPER and INDULT I-class
   feasts too (patron/titular/dedication days, {!is_universal}/{!is_indult}
   below), which arrive via `data/ef/adjustments.sexp`-style overlays, this
   architecture's own advertised extension point -- a diocesan I-class
   patronal feast landing 7-13 January would have made the inversion live
   immediately, transferring the I-class feast as though a II-class Sunday
   had impeded it. Demonstrated directly (fix round 1, reproduced then
   reverted): a synthetic Class1 sanctoral candidate on 11 January was
   OBSERVED as Holy Family (band -14) and the synthetic candidate itself
   TRANSFERRED to the next day -- backwards on every count.

   FIXED: every {!band} branch now returns the real RG 91 entry number
   TIMES TEN (entry 1 -> 10, entry 28 -> 280, {!unclassified} untouched at
   [max_int]) instead of the bare 1..28 -- not an arbitrary rescale, a
   deliberate one: RG 91's own text records a "primum mobilia, deinde
   fixa" (movable-then-fixed) split at THREE rows, not only this one --
   entry 12's own ninth sub-item ("Festa indulta I classis, primum
   mobilia, deinde fixa"), entry 14 (this one), and entry 20 ("indult
   II-class feasts (movable then fixed)", register §4's own summary) --
   and only entry 14 has a live witness in this codebase's data today. The
   ×10 scale reserves nine integers of genuine headroom immediately BEFORE
   each real entry's own value for exactly this kind of sub-rank, without
   requiring a second rescale if entry 12's or entry 20's own movable half
   ever needs one too -- a value DERIVABLE from RG 91's own table structure,
   not an arithmetic convenience chosen to dodge one collision. [band]'s own
   entry-14 comments (below) restate this scale at each branch it touches. *)
let entry_14_fixed_band = 140

(* RG 91 entry 14's own text (scan-verified, docs/research/rules-register.md
   §4): "14. Festa Domini II classis, PRIMUM MOBILIA, DEINDE FIXA" -- feasts
   of the Lord, II class, MOVABLE FIRST, then fixed. Both halves are the
   SAME numbered table row, but the primary text states a real priority
   between them, not merely two disjoint categories -- [band]'s own
   entry-14 branches below must not literally TIE at
   {!entry_14_fixed_band}, because a tie would fall to
   {!Precedence.resolve}'s own kernel-level fallback ([compare_by],
   Slug.compare), which is deliberately NOT rubric-authoritative (the exact
   anti-pattern RG 113's own fix already corrected once for commemoration
   ordering, this file's own header) and would in fact pick the WRONG side
   here: "commemoration-of-the-baptism-of-the-lord" sorts before
   "ef-time-after-epiphany-sunday-1" alphabetically, backwards from "primum
   mobilia". Derived from {!entry_14_fixed_band}, one better (lower) than
   it, using the one integer of headroom the ×10 scale reserves immediately
   before every real entry -- not a citation to some entry "13.5" that does
   not exist in the primary text, just the ordering fact RG 91's own two
   half-rows require, expressed the same way the whole table now is. The
   one real witness today: Holy Family (RG 17(b), temporal_ef.ml's own
   [holy_family_sunday]) against the fixed Commemoration of the Baptism of
   the Lord (13 January) -- the only date they can ever coincide, since
   Holy Family only ever falls 7-13 January and no OTHER fixed Class2-Lord
   sanctoral entry shares that window (register §6.0's own subject audit:
   Transfiguration 6 Aug, Exaltation of the Cross 14 Sep, Dedication of the
   Archbasilica 9 Nov, the Purification 2 Feb, the Baptism 13 Jan -- the
   Baptism alone falls in Holy Family's own window). *)
let entry_14_movable_band = entry_14_fixed_band - 1

let is_indult layer = String.starts_with ~prefix:indult_prefix layer
let is_universal layer = String.equal layer universal_layer

(* Not an RG citation either -- see [universal_layer] above. Nothing in
   {!Celebration.t} otherwise marks "this is a vigil, not an ordinary office
   of the same rank" (see the file's top comment), so entries 21/26 read it
   off the temporal cycle's own slug suffix (rite_ef/temporal_ef.ml's
   [named], e.g. "ef-ascension-vigil"). *)
let vigil_suffix = "-vigil"

(* Not an RG citation -- see [universal_layer]. Task 10's sanctoral bootstrap
   turned out to name its four real vigils with lectio's OWN convention, a
   "vigil-of-X" PREFIX (data/ef/sanctoral.sexp: vigil-of-st-lawrence,
   vigil-of-sts-peter-paul, vigil-of-the-assumption, vigil-of-the-nativity-
   of-st-john-the-baptist), not [vigil_suffix] -- exactly the mismatch Task
   7's review predicted when it asked for [vigil_suffix] to be exposed.
   [is_vigil] below checks both conventions, so a celebration is a "vigil"
   for RG 91/33's purposes regardless of which layer (temporal or sanctoral)
   produced it. *)
let vigil_prefix = "vigil-of-"

let is_vigil slug =
  String.ends_with ~suffix:vigil_suffix slug || String.starts_with ~prefix:vigil_prefix slug

(* Not an RG citation -- see [universal_layer]. Entry 18's Ember days are
   identified by the temporal cycle's own slug convention (rite_ef/
   temporal_ef.ml's [ember]: "ef-<set>-ember-<day>"), not re-derived here:
   the September anchor in particular is one of the more contested dates in
   the 1962 calendar (temporal_ef.ml's own comment on
   [third_sunday_of_september]), and re-deriving it a second time would only
   create a second place for that same uncertainty to drift. Only the
   Advent, Lent and September sets are listed: RG 91 entry 18 names exactly
   those three; the Whitsun (Pentecost) set is I class and falls inside the
   Pentecost octave, entry 10, matched below before this is ever reached.
   Exposed for the same reason as [vigil_suffix]: a rename of temporal_ef's
   format has somewhere to be caught other than a silently-wrong entry 18.

   [september_ember_prefix] is broken out as its own name (rather than an
   anonymous list literal) because Task 9's [privilege_of] needs to test the
   September set alone: RG 109(e)'s three named seasons (Advent, Lent,
   Passiontide, §4 "Commemorations") never include September, which sits
   entirely in time after Pentecost under any reading -- so September Ember
   days need their own separate privilege category, (d), regardless of how
   (e) itself is read. CORRECTED (fix round 1, F1/F2): this comment
   previously justified the split the other way round, claiming RG 109(e)
   privileges September specifically "while leaving the Advent and Lent
   sets ordinary" -- WRONG; see [privilege_of]'s own (e) comment below for
   the full argument. The Advent and Lent Ember sets ARE privileged under
   (e), the same as any other Advent/Lent feria; building [ember_prefixes]
   from this constant rather than duplicating the literal keeps the two
   from silently drifting apart. *)
let advent_ember_prefix = "ef-advent-ember-"
let lent_ember_prefix = "ef-lent-ember-"
let september_ember_prefix = "ef-september-ember-"
let ember_prefixes = [ advent_ember_prefix; lent_ember_prefix; september_ember_prefix ]

let is_ember_18 slug = List.exists (fun prefix -> String.starts_with ~prefix slug) ember_prefixes

let band (ctx : Vocab_ef.season Precedence.context) (c : Vocab_ef.rank Precedence.candidate) :
    int =
  let cel = c.Precedence.cel in
  let rank = cel.Celebration.rank in
  let status = cel.Celebration.status in
  let subject = cel.Celebration.subject in
  let layer = cel.Celebration.layer in
  let slug = Slug.to_string cel.Celebration.slug in
  let is_temporal = c.Precedence.origin = Precedence.Temporal in
  let is_vigil = is_vigil slug in
  let date = ctx.Precedence.date in
  let season = ctx.Precedence.season in
  let weekday = ctx.Precedence.weekday in
  let is_sunday = weekday = Date.Sun in
  let m = Date.month date and d = Date.day date in
  (* Easter offset, the same convention as temporal_ef.ml's [days_between
     easter d]: 0 is Easter itself, negative before, positive after. *)
  let off = Date.to_rata date - Date.to_rata (Computus.gregorian_easter (Date.year date)) in
  (* Named so entry 8's Sunday exception below can read "one worse than the
     Sunday it must yield to" rather than a bare integer that happens to
     equal entry 15's own value; entry 15's own branch returns this same
     binding, not a second literal, so the two can never drift apart.
     CORRECTED, fix round 1 (coordinator finding 1): every branch below now
     returns the real RG 91 entry number TIMES TEN, not the bare 1..28 --
     see {!entry_14_fixed_band}'s own comment for why (a genuine ordering
     bug the bare scale could not express, not a cosmetic rename). *)
  let entry_15_band = 150 in
  let open Vocab_ef in
  (* CORRECTED (Task B fix round 1, ef-rg16a): a [Commemoration_only]
     celebration has NO row in RG 91's table at all, checked FIRST, ahead of
     every rank-keyed branch below -- RG 91's own text enumerates only "dies
     liturgici" (entry 24's own wording, e.g., "Festa III classis, in
     calendario Ecclesiae universae inscripta" -- FEASTS, inscribed in the
     calendar), and the calendarium itself marks the difference in its own
     notation: 22 September's row reads "S. Thomae de Villanova Ep. et
     Conf., III classis. / Commemoratio Ss. Mauritii et Soc. Mm." -- Thomas
     gets a class number (a "festum"); Maurice gets "Commemoratio" and NO
     class number at all, because he never had a row in the table for that
     occasion to begin with. Before this fix, [band] read [rank] alone, so
     a [Commemoration_only] entry silently borrowed the SAME entry number
     as a genuine [Feast] of its own rank (a Class3 Commemoration_only
     entry banded to 24, indistinguishable from a real Class3 universal
     feast) -- manufacturing a "tie" at {!compare_precedence}/RG 113's own
     admission ordering that the primary text never creates: RG 113 does
     not run out of instruction between two same-rank commemorations;
     [band] ran out of fidelity, handing out a table row that does not
     exist for one of them. [unclassified] (worse than every real entry) is
     the same value already used for a candidate this table's 28 branches
     otherwise fail to describe, which is exactly the right answer here
     too: "not in this table" for a different reason, same table-position
     consequence. Confirmed by the oracle: missalemeum's own commemoration
     id embeds a rank that AGREES with the demoted status this represents
     (e.g. sancti:09-22o:4:r for Maurice, rank 4, against Thomas's own
     sancti:09-22:3:w, rank 3, on the day Thomas is observed -- test_oracle
     .ml's own [test_identity_rank_corroboration]). Restricted to [status];
     [rank] itself is untouched, and stays load-bearing for a demoted feast:
     RG 111(b)'s "scilicet de festo II classis" floor ({!admit} below) reads
     [rank] directly, and it is exactly that floor which excludes these
     Class3 entries from a II-class Sunday's single slot. Only ORDERING among
     candidates already offered to {!admit} changes here.
     NOTE, corrected: this comment previously cited {!Celebration.status}'s
     own doc comment as saying a demoted feast retains its rank for RG 111's
     admission-COUNT purposes. That misquotes it -- celebration.mli says RG
     111 "orders admitted commemorations by dignity", i.e. it names the
     ORDERING use this guard removes, not a counting one. celebration.mli's
     line is itself now stale: ordering moved from dignity to [band] in
     ea22ad2. The guard is right; its former justification was not. *)
  if status = Celebration.Commemoration_only then unclassified
    (* 1: Nativity, Easter Sunday, Pentecost Sunday (I class w/ octave). *)
  else if is_temporal && rank = Class1 && ((m = 12 && d = 25) || off = 0 || off = 49) then 10
    (* 2: Sacred Triduum (Thu-Sat of Holy Week). *)
  else if is_temporal && rank = Class1 && off >= -3 && off <= -1 then 20
    (* 3: Epiphany, Ascension, Holy Trinity, Corpus Christi, Sacred Heart,
       Christ the King. *)
  else if is_temporal && rank = Class1
          && ((m = 1 && d = 6) (* Epiphany *)
             || off = 39 (* Ascension *) || off = 56 (* Trinity *)
             || off = 60 (* Corpus Christi *) || off = 68 (* Sacred Heart *)
             || Date.compare date (Temporal_ef.christ_the_king (Date.year date)) = 0)
  then 30
    (* 4: Immaculate Conception, Assumption BVM. *)
  else if (not is_temporal) && rank = Class1 && ((m = 12 && d = 8) || (m = 8 && d = 15)) then 40
    (* 5: Vigil & Octave day of the Nativity. *)
  else if is_temporal && rank = Class1 && ((m = 12 && d = 24) || (m = 1 && d = 1)) then 50
    (* 6: Sundays of Advent, Lent, Passiontide, and Low Sunday. *)
  else if is_temporal && rank = Class1 && is_sunday
          && (season = Advent || season = Lent || season = Passiontide || off = 7)
  then 60
    (* 7: I-class ferias not above -- Ash Wednesday; Mon/Tue/Wed of Holy Week.
       Thu-Sat of Holy Week are the Triduum, entry 2 above, not this entry. *)
  else if is_temporal && rank = Class1 && (off = -46 || (off >= -6 && off <= -4)) then 70
    (* 8: All Souls -- RG 91 entry 8's own text (§4) carries a qualifier this
       transcription must honour: "yields to an occurring Sunday". 2 November
       is always Time_after_pentecost (well clear of Advent/Lent/Passiontide
       and of every other entry's own Easter-relative or fixed date), so a
       Sunday landing on it is always an ordinary entry-15 II-class Sunday --
       the one and only rival this exception ever has to lose to. On such a
       Sunday this returns [entry_15_band + 1]: strictly worse than 150 (an
       exact tie would fall to Precedence.resolve's slug tie-break, which
       for "ef-all-souls" against a "ef-time-after-pentecost-sunday-*" slug
       would make All Souls WIN -- the precise bug this guards against), but
       otherwise not a citation to any other RG 91 row -- nothing else can
       ever occur on 2 November to be confused with it. Entry 8's own [rank]
       is untouched by this, so Task 8's disposition (RG 95: only I-class
       feasts transfer) still sees the true I-class candidate it needs to
       move to 3 November. *)
  else if (not is_temporal) && rank = Class1 && m = 11 && d = 2 then
    if is_sunday then entry_15_band + 1 else 80
    (* 9: Vigil of Pentecost. *)
  else if is_temporal && rank = Class1 && off = 48 then 90
    (* 10: Days within the Octaves of Easter and Pentecost. *)
  else if is_temporal && rank = Class1 && ((off >= 1 && off <= 6) || (off >= 50 && off <= 55))
  then 100
    (* 11: I-class feasts of the universal Church not above. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class1 && is_universal layer then 110
    (* 12: Proper I-class feasts. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class1 && not (is_indult layer) then 120
    (* 13: Indult I-class feasts. By elimination once 11 and 12 have failed:
       not the universal layer (11), and marked as an indult overlay (12's
       "not indult" test having just failed). *)
  else if (not is_temporal) && (not is_vigil) && rank = Class1 then 130
    (* 14, MOVABLE half -- RG 91 entry 14's own "primum mobilia, deinde
       fixa" ({!entry_14_movable_band}'s own comment has the full citation
       and reasoning). Checked before the FIXED half immediately below,
       matching the primary text's own order, though [is_temporal] already
       makes the two branches structurally disjoint on any one candidate
       regardless of which is checked first -- the ordering here is for a
       reader following RG 91's own prose, not for correctness. The only
       witness this codebase currently constructs is Holy Family
       (temporal_ef.ml's own [holy_family_sunday] branch, the one place
       [subject] is ever [Lord] on a temporal-origin candidate); a
       hypothetical movable Holy-Name-of-Jesus office (RG 17(a), still
       unbuilt as its own named day, register §6) would reach this same
       branch too, once built, since nothing here is keyed to Holy Family's
       own slug. *)
  else if is_temporal && rank = Class2 && subject = Subject.Lord then entry_14_movable_band
    (* 14, FIXED half -- RG 91 entry 14, deliberately UNQUALIFIED (contrast
       entry 16, which explicitly says "not of the Lord"; RG 37c (§4,
       "Sundays") speaks of "II-class feasts of the Lord" replacing an
       occurring II-class Sunday with no universal qualifier either). No
       layer test here, unlike 11/12/13 and 16/19/20: the register does not
       split this entry into universal/proper/indult, so a proper or
       indult feast of the Lord still bands {!entry_14_fixed_band}, not
       19/20. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class2 && subject = Subject.Lord then
    entry_14_fixed_band
    (* 15: Sundays, II class (every Sunday not already named at 6). *)
  else if is_temporal && rank = Class2 && is_sunday then entry_15_band
    (* 16: II-class feasts of the universal Church, not of the Lord. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class2 && is_universal layer then 160
    (* 17: Days within the Octave of the Nativity (26-28 Dec are Stephen,
       John, the Innocents -- sanctoral, not this entry). *)
  else if is_temporal && rank = Class2 && m = 12 && (d = 29 || d = 30 || d = 31) then 170
    (* 18: II-class ferias -- Advent 17-23 Dec; Ember days of Advent, Lent,
       September. *)
  else if is_temporal && rank = Class2
          && ((season = Advent && m = 12 && d >= 17 && d <= 23) || is_ember_18 slug)
  then 180
    (* 19: Proper II-class feasts. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class2 && not (is_indult layer) then 190
    (* 20: Indult II-class feasts. By elimination, as at 13. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class2 then 200
    (* 21: II-class vigils (Ascension, Assumption, John Baptist, Peter & Paul
       -- can be temporal- or sanctoral-origin, see the file comment above). *)
  else if rank = Class2 && is_vigil then 210
    (* 22: Ferias of Lent and Passiontide (Thursday after Ash Wednesday to the
       Saturday before Palm Sunday), except the Ember days (18 above). *)
  else if is_temporal && rank = Class3 && (season = Lent || season = Passiontide) then 220
    (* 23: III-class feasts in particular calendars. Unlike 11/12 and 14/16
       above, the universal entry (24) is the HIGHER number here -- RG 91's
       own table ranks a particular-calendar III-class feast ahead of a
       universal one, the reverse of the I/II-class ordering. Transcribed as
       the register states it, not "corrected" into the other classes'
       pattern. RG 91 has no indult sub-rank at III class, so every non-base
       layer lands here, not split further. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class3 && not (is_universal layer) then 230
    (* 24: III-class feasts in the universal calendar. *)
  else if (not is_temporal) && (not is_vigil) && rank = Class3 then 240
    (* 25: Ferias of Advent to 16 Dec, except the Ember days (18 above). *)
  else if is_temporal && rank = Class3 && season = Advent then 250
    (* 26: III-class vigils (St Lawrence). *)
  else if rank = Class3 && is_vigil then 260
    (* 27: Office of the BVM on Saturday -- every otherwise-unoccupied IV-class
       Saturday, per the historical default that fills it; ordinary Mass
       propers still make Rogation Mon/Tue/Wed proper without changing the
       Office (RG 88, see temporal_ef.ml's [temporal]), so those never carry
       this entry unless they happen to fall on the Saturday itself. Excludes
       vigils for the same reason 11-13/14/16/19/20/23/24 do: RG 91 has no
       IV-class vigil at all (RG 91's own vigil list, §4 "Vigils",
       stops at III class), so one would be an anomaly, not this entry. *)
  else if is_temporal && (not is_vigil) && rank = Class4 && weekday = Date.Sat then 270
    (* 28: IV-class ferias -- the unqualified catch-all (temporal_ef.ml's own
       comment on [ferial_rank] cites the same primary text, "Feriae IV
       classis"). Excludes vigils for the same reason as 27 above: a IV-class
       "feria" that is also a vigil is not a feria RG 91 describes. *)
  else if (not is_vigil) && rank = Class4 then 280
  else unclassified

(* Task 8: what happens to the day's LOSING candidate (docs/research/
   rules-register.md §4, "Occurrence" RG 92-95 and "Vigils" RG 33, plus RG
   94; also §6.0/Caput III "De Dominicis" RG 16(a), below). [band] above
   decides who wins; this decides the loser's fate, which turns on the
   LOSER's own rank and status (RG 95), except RG 33's vigil omission and RG
   16(a)'s Sunday-suppression, which also have to read the winner. Nothing
   here ever returns [Precedence.Repose]: that disposition denotes RG
   100-102's *repositio* (perpetual impediment from a proper/diocesan
   calendar), out of this plan's scope -- see calendar.mli's own note that
   nothing in the EF ruleset currently emits it. *)

(* RG 33 -- CORRECTED 2026-08-12 (Task 16, primary-source-verified against
   docs/research/1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum,_LT.pdf, the
   General Rubrics' own Chapter XI "De Vigiliis"). The register previously
   transcribed this as "a I/II-class vigil is entirely omitted"; the
   PRIMARY TEXT reads the other way round:

     "33. Vigilia II aut III classis penitus omittitur, si occurrat in
     dominica quavis, aut in festo I classis, vel si festum cui
     præmittitur in alium diem transferri aut ad commemorationem reduci
     contingat."

   -- "A vigil of the II OR III class is entirely omitted, if it occurs on
   ANY Sunday whatsoever, or on a feast of the I class, or if the feast it
   precedes happens to be transferred to another day or reduced to a
   commemoration." I-class vigils (Nativity, Pentecost, RG 30) are outside
   this rule entirely -- RG 30's own text says they "festis quibuslibet
   præferunt, et nullam admittunt commemorationem" (are preferred to ANY
   feast whatsoever, and admit no commemoration at all), i.e. they can never
   lose in the first place: {!band} entries 1/5/9 already rank Nativity Eve
   and the Pentecost Vigil above every Sunday and every other I-class row
   that could coincide with their fixed/Easter-relative dates (verified: no
   date collision is even representable), so no I-class vigil can ever reach
   this function as a [loser] -- the branch below never needs to test for
   [Class1] and, before this fix, its stray inclusion of [Class1] here was
   simply dead code, not a second bug (see the task report for the
   argument). The bug was the OTHER half: [Class3] (the sole III-class
   vigil, St Lawrence, RG 32) was MISSING from this branch, so it fell
   through to the generic "commemorated or omitted" branch below instead of
   RG 33's mandatory omission -- confirmed wrong for real data: 9 August
   2026 is a Sunday, and before this fix "vigil-of-st-lawrence" competed for
   (and could in principle win) that Sunday's single commemoration slot,
   when RG 33 says it must not even be a candidate. The oracle comparison
   (missalemeum, Task 16) independently confirms: 9 Aug 2026 shows no trace
   of the vigil surviving as a commemoration.

   The third omission trigger in RG 33's own text -- "or if the feast it
   precedes is transferred to another day or reduced to a commemoration" --
   is NOT implemented: no II/III-class vigil's own feast (Ascension,
   Assumption, John Baptist, Sts Peter & Paul, Lawrence) is ever
   transferred or reduced to a commemoration anywhere in this codebase's
   current data (all fixed I-class, none coincide with anything of equal or
   higher rank within any year this project has sampled), so no witness
   exists to build or test this clause against; flagged in the register
   (§6) rather than guessed. *)
let is_omissible_vigil (rank : Vocab_ef.rank) = rank = Vocab_ef.Class2 || rank = Vocab_ef.Class3

(* Every Sunday slug this rite's temporal cycle produces -- named
   (temporal_ef.ml's [named], e.g. "ef-easter-sunday") or the generic
   "ef-<season>-sunday-<n>" fallback ([sunday_slug]) -- contains this
   marker; nothing else [band] classifies does. Not an RG citation itself --
   see [universal_layer]'s note on this file's own naming conventions --
   exposed for the same reason as {!vigil_suffix}: a future rename of
   temporal_ef's Sunday-slug format has somewhere to be caught other than a
   silently-wrong RG 33 disposition. *)
let sunday_marker = "-sunday"

let contains_substring s ~needle =
  let ls = String.length s and ln = String.length needle in
  let rec at i = i + ln <= ls && (String.sub s i ln = needle || at (i + 1)) in
  ln = 0 || at 0

let is_sunday_slug slug = contains_substring slug ~needle:sunday_marker

(* RG 33's own two conditions, taken directly from its text ("any Sunday or
   a I-class feast") -- NOT derived from anything about which RG 91 entries
   can numerically outrank a vigil. [rank = Class1] is the "I-class feast"
   half. [is_sunday_slug] is the "any Sunday" half, and it is not redundant
   with the rank check: RG 91 entries 14 and 16-20 (Feasts of the Lord II
   class, universal/proper/indult II-class feasts, days within the Nativity
   octave) are all [Class2], all outrank a II-class vigil (entry 21), and
   none of them is a Sunday -- a winner of that shape satisfies neither
   condition here, so [impedes_vigil] correctly returns [false] and such a
   vigil falls through to RG 95's ordinary commemorate-or-omit branch
   instead of RG 33's omission, exactly as the rubric requires. *)
let impedes_vigil (winner : Vocab_ef.rank Precedence.candidate) =
  let cel = winner.Precedence.cel in
  cel.Celebration.rank = Vocab_ef.Class1
  || is_sunday_slug (Slug.to_string cel.Celebration.slug)

(* RG 91 entry 17's own slug convention (rite_ef/temporal_ef.ml's [named]:
   "ef-nativity-octave-day-%d" for 29-31 Dec -- 26-28 Dec are Stephen, John,
   the Innocents, sanctoral, and never carry this prefix, see [band]'s entry
   17 comment). Not an RG citation itself -- see [universal_layer] -- reused
   below by [privilege_of] for RG 109(c). *)
let nativity_octave_prefix = "ef-nativity-octave-day-"

(* RG 109's own three named seasons for (e), "of ferias of Advent, Lent and
   Passiontide" (§4, "Commemorations") -- temporal_ef.ml's generic
   <season>-<week>-<weekday> ferial fallback slugs, whose season word is
   [season_slug_word]'s output for exactly these three (vocab_ef.ml: Advent
   and Passiontide are unmodified [season_to_string]; Lent likewise). Also
   matches the Lent "after Ashes" sub-case ("ef-lent-after-ashes-<weekday>",
   temporal_ef.ml's own [christmastide_feria_slug]-adjacent branch), which
   is still a Lent feria under this same prefix. Not an RG citation -- see
   [universal_layer] -- private: nothing outside [privilege_of] needs it. *)
let alp_feria_prefixes = [ "ef-advent-"; "ef-lent-"; "ef-passiontide-" ]

(* RG 109 (docs/research/rules-register.md §4, "Commemorations"): the
   closed list of privileged commemorations, checked in the register's own
   lettered order. A candidate matching none of (a)-(f) is ordinary, per the
   register's own closing sentence, "All others are ordinary." Read entirely
   off the candidate's own fields (rank, slug, origin) -- no [context]
   (date/season/weekday) is available or needed: every category names a
   property of the commemorated OFFICE ITSELF ("a commemoration OF a
   Sunday", "OF a I-class day", ...), not of the day it happens to fall on,
   and each of (a)-(e) already has a candidate-only marker this file's own
   conventions establish ([sunday_marker], rank, [nativity_octave_prefix],
   [september_ember_prefix]/[alp_feria_prefixes]) -- see the task report for
   the full reasoning.

   [disposition] below is this function's only caller, at both of its
   [Commemorate] sites -- replacing Task 8's [interim_privilege] placeholder,
   which always returned [Ordinary] regardless of the loser's real shape.
   [admit] (RG 108-111's admission counts, below) trusts the privilege value
   [disposition] has already attached rather than recomputing it here a
   second time. *)
let privilege_of (c : Vocab_ef.rank Precedence.candidate) : Precedence.privilege =
  let cel = c.Precedence.cel in
  let rank = cel.Celebration.rank in
  let slug = Slug.to_string cel.Celebration.slug in
  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". *)
  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
     [Class1] candidate ever reaches [Commemorate] at all is via
     [Celebration.status = Commemoration_only] (a plain [Feast]-status
     [Class1] loser always [Transfer]s instead, RG 95, below) -- so this
     branch is real but its only reachable witness today is that shape; see
     the task report. *)
  else if rank = Class1 then Precedence.Privileged
  (* (c) RG 109(c) (§4): "of days within the Octave of the Nativity". *)
  else if is_temporal && String.starts_with ~prefix:nativity_octave_prefix slug then
    Precedence.Privileged
  (* (d) RG 109(d) (§4): "of September Ember days" -- named on its
     own because September falls entirely outside (e)'s three seasons
     (Advent/Lent/Passiontide) under ANY reading, not because it needs
     excluding FROM (e) the way review round 1's F1/F2 finding corrected
     the Advent/Lent Ember sets below to no longer need. *)
  else if is_temporal && String.starts_with ~prefix:september_ember_prefix slug then
    Precedence.Privileged
  (* (e) RG 109(e) (§4): "of ferias of Advent, Lent and Passiontide" --
     CORRECTED, fix round 1 (F1/F2): this branch previously excluded the
     Advent and Lent Ember sets via [not (is_ember_18 slug)], reading RG
     109(e)'s bare "feriis Adventus, Quadragesimae" as tacitly narrower than
     the ordinary ferias of those seasons, on the theory that (d)'s separate
     September carve-out implied Ember days needed excluding from (e) too.
     That reading does not survive comparing (e)'s text against RG 91's own
     TABLE entries for the same seasons (register §4, "Ferias of Lent and
     Passiontide... EXCEPTIS feriis Quatuor Temporum" at entry 22; "Ferias
     of Advent... EXCEPTIS feriis Quatuor Temporum" at entry 25): the table
     needs an explicit "exceptis" to keep Ember days from being double-
     listed at both their own entry 18 AND entries 22/25 -- and an explicit
     exception is only necessary because, ABSENT one, "feriae Adventus"/
     "feriae Quadragesimae" already DO include their Ember sub-days by
     default (an unnecessary exception is not how a rubrical text is
     drafted). RG 109(e) carries no such "exceptis" clause, so its bare
     "feriis Adventus, Quadragesimae" is read at that same default,
     INCLUSIVE scope: the Advent and Lent Ember ferias ARE "ferias of
     Advent"/"of Lent" in RG 109(e)'s sense, hence privileged, not merely
     ordinary. (d)'s own separate existence is unaffected by this reading
     either way -- September Ember days sit in "time after Pentecost",
     never within Advent/Lent/Passiontide under any reading, so (d) remains
     necessary regardless; it is not evidence for excluding Advent/Lent
     Ember from (e), only for including September at all.) Consequently
     [is_ember_18] is no longer tested here -- an Advent/Lent Ember slug
     matches this branch exactly like an ordinary Advent/Lent feria slug
     does, via the same [alp_feria_prefixes] prefix test; only a September
     Ember slug is structurally excluded, because "ef-september-ember-*"
     never starts with any of [alp_feria_prefixes] ("ef-advent-"/"ef-lent-"/
     "ef-passiontide-") in the first place -- (d) above already privileges
     it under its own name. *)
  else if is_temporal && List.exists (fun p -> String.starts_with ~prefix:p slug) alp_feria_prefixes then
    Precedence.Privileged
  (* (f) RG 109(f) (§4): "of the Major Rogations, in Mass" -- the
     Major Litanies (25 April, RG 80) are STILL not computed anywhere in
     this codebase (temporal_ef.ml's own comment on [temporal]'s Rogation
     branch, CORRECTED final fix wave item 7: they did not, in fact,
     "arrive with Plan 3's sanctoral" -- Plan 3 shipped without them,
     register §6 tracks this as an open item with no plan yet committed to
     build it), so no candidate this engine can currently construct
     represents one. There is no existing slug
     convention to anchor a check to, and guessing one risks silently
     misclassifying whatever a future task does name it -- a wrong citation
     is worse than a missing one, so this is left unimplemented and flagged
     in the task report rather than guessed. Deliberately NOT matched by
     anything above: the Minor Litanies/Rogations ("ef-rogation-monday"/
     "-tuesday", RG 87) temporal_ef.ml DOES compute are a different
     observance RG 109(f) does not name (RG 88: the Minor Rogations change
     nothing in the Office at all), so they correctly fall through to
     "ordinary" below, not this category. *)
  else Precedence.Ordinary

let disposition ~(winner : Vocab_ef.rank Precedence.candidate)
    ~(loser : Vocab_ef.rank Precedence.candidate) : Precedence.disposition =
  let open Vocab_ef in
  let cel = loser.Precedence.cel in
  let is_temporal = loser.Precedence.origin = Precedence.Temporal in
  if cel.Celebration.status = Celebration.Commemoration_only then
    (* Always -- checked before RG 33's omission and RG 95's transfer so
       neither can override it: a Commemoration_only entry can never win
       (Precedence.resolve holds it out of the band contest entirely, see
       that module's [resolve]) and, per the brief, can never transfer
       either. Its privilege is [privilege_of loser] like every other
       [Commemorate] below -- Commemoration_only carries a real [rank] for
       exactly this purpose (Celebration.mli: "RG 111 orders admitted
       commemorations by dignity"), so RG 109(b) applies to it precisely as
       it would to any other candidate. *)
    Precedence.Commemorate (privilege_of loser)
  else if
    is_omissible_vigil cel.Celebration.rank
    && is_vigil (Slug.to_string cel.Celebration.slug)
    && impedes_vigil winner
  then
    (* RG 33, corrected (see {!is_omissible_vigil}'s own comment): II- or
       III-class vigils only -- a real I-class vigil can never reach this
       function as a loser at all (see that comment), so this branch would
       never have fired for [Class1] even before the fix; what changed is
       that [Class3] (St Lawrence) now correctly reaches RG 33's omission
       instead of falling through to the generic "commemorated or omitted"
       branch below. *)
    Precedence.Omit
  else if
    cel.Celebration.rank = Class1
    && not (is_sunday_slug (Slug.to_string cel.Celebration.slug))
  then
    (* RG 95 (§4, "Occurrence" and "Transfer/translation"): only I-class FEASTS have the right
       of translation -- RG 91's own table lists Sundays as a separate row
       (entry 6) from feasts (entries 11-13), so a Sunday is never a "feast" in RG 95's sense, and
       [is_sunday_slug] (the same marker RG 33's [impedes_vigil] and RG
       109(a)'s [privilege_of] already use) excludes it here. This is the
       branch that completes Task 7's All Souls fix (RG
       91 entry 8): All Souls is I class, not a vigil, and not a Sunday
       slug, so once it loses to an occurring Sunday it still reaches here
       and transfers -- to 3 November, now DIRECTLY authorised by RG 96
       Attamen (b) (primary-source-verified 2026-08-12): "Commemoratio
       omnium Fidelium defunctorum, quando occurrit cum dominica,
       transfertur, tamquam in sedem propriam, in feriam II sequentem" --
       when it coincides with a Sunday, transferred, as to its own proper
       seat, to the following Monday. Previously this rested only on entry
       8's own parenthetical plus the general RG 96 walk, which happened to
       produce the right date; WHERE it lands either way is
       Rite.transfer_target's job, not this function's -- disposition only
       says THAT it moves. *)
    Precedence.Transfer
  else if
    is_temporal
    && (not (is_vigil (Slug.to_string cel.Celebration.slug)))
    && cel.Celebration.rank = Class4
  then
    (* CORRECTED, fix round 1 (F1/F2 -- both real, the second the direct
       cause of the first): the branch this replaces gated on
       [privilege_of loser = Ordinary], justified by treating RG 109 as an
       "exhaustive, closed list of the only temporal-origin circumstances
       that ever generate a commemoration". That justification does not
       survive reading RG 109 itself: it is headed "Commemorationes
       PRIVILEGIATAE sunt commemorationes" and closes "Omnes aliae
       commemorationes sunt commemorationes ORDINARIAE" -- it sorts
       commemorations that ALREADY exist into two HONOUR classes
       (privileged vs ordinary, RG 108's differing liturgical hours), and
       says nothing about which offices have the RIGHT to be commemorated
       in the first place. Testing [privilege_of = Ordinary] as an
       ELIGIBILITY gate therefore happened to reach the right answer for
       IV-class ferias (they are never commemorated, but for a reason
       RG 109 does not state) and the WRONG answer for II- and III-class
       ferias impeded during a season RG 109(e) does not privilege by name
       (Advent 17-23 Dec's own ordinary-non-Ember ferias were fine, already
       matching (e)'s slug prefix; the Advent and Lent EMBER ferias were
       not, since the pre-fix (e) excluded them -- see [privilege_of]'s own
       fix-round-1 comment above, which independently corrects THAT half
       too). Confirmed wrong by direct reproduction (fix-round-1 review):
       1900-12-21 (an Advent Ember Friday, RG 91 entry 18, II class) lost
       its own commemoration entirely under the pre-fix code, while an
       ORDINARY (non-Ember, lower-solemnity) Advent feria the same week
       kept its commemoration -- backwards on any reading.

       The actual rule is Caput IV, "De feriis" (RG 21-27), which the
       original Task 16 pass never opened -- a FERIAL-CLASS-keyed rule,
       entirely separate from RG 109's HONOUR-class one:
       - RG 23 (I-class ferias -- Ash Wednesday, Holy Week): "nullam
         admittunt commemorationem, nisi unam privilegiatam" -- admit no
         commemoration except one privileged one. Never actually reaches
         this function as a loser (these ferias structurally always
         outrank anything that could coincide with their dates -- {!band}
         entries 2/7, see that function's own file comment and the Task 11
         Easter-window invariant), so this clause has no live witness, the
         same as before.
       - RG 24 (II-class ferias -- Advent 17-23 Dec, the Advent/Lent/
         September Ember ferias, RG 91 entry 18): "si vero impediuntur,
         COMMEMORARI DEBENT" -- if indeed impeded, they MUST be
         commemorated. Not optional, not conditioned on RG 109's list.
       - RG 25 (III-class ferias -- ordinary Lent/Passiontide ferias, RG 91
         entry 22; ordinary Advent ferias to 16 Dec, entry 25): "Hae
         feriae, si impediuntur, commemorari debent" -- same mandate.
       - RG 26: "Omnes feriae, numeris 23-25 non nominatae, sunt feriae IV
         classis; quae NUNQUAM COMMEMORANTUR" -- every feria not named in
         23-25 is IV class, and IV-class ferias are NEVER commemorated.
         This is [ferial_rank]'s own unqualified IV-class catch-all
         (temporal_ef.ml), covering the ordinary green-season ferias of
         Time after Epiphany/Pentecost, Septuagesima, Paschaltide outside
         its privileged octave, and the Minor Rogation days (RG 87/88 --
         they change nothing in the Office, so they take their season's
         plain ferial class, which for Paschaltide-adjacent dates is
         IV, not a special one).

       So this branch is now gated directly on RG 26's own condition
       ([rank = Class4]), which is the ONLY ferial class RG 21-27 excludes
       from commemoration -- Class1 is structurally unreachable here (RG
       23, above); Class2 and Class3 both fall through to the final
       [Commemorate] branch below (RG 24/25's mandate), tagged with
       whatever HONOUR class [privilege_of] separately computes for them
       under RG 109 -- a question this branch no longer conflates with
       eligibility. SANCTORAL losers are entirely unaffected (the
       [is_temporal] guard): Caput IV governs FERIAE, RG 21's own opening
       definition ("Nomine feriae intelleguntur singuli dies hebdomadae,
       praeter dominicam"), never a saint's day; RG 111(c)/(d) admit an
       "ordinary" commemoration of a losing SAINT freely, with no such
       class-keyed gate.

       Empirically confirmed against the missalemeum oracle (Task 16,
       2026-2027, both years): every one of ~190 days where a saint's feast
       impedes an ordinary (IV-class, non-privileged) temporal feria shows
       ZERO commemorations in the oracle (e.g. "St. Marcellus I" impeding
       the plain "Friday after Epiphany"), and the SAME rank-4 gate,
       independently, correctly still omits the Minor Rogation days (RG 87)
       losing to a saint -- both consequences of RG 26 alone now, not of a
       reading of RG 109 that RG 109's own text does not support.

       [is_vigil] is EXCLUDED from this branch for the same reason as
       before, restated under the corrected citation: a II/III-class vigil
       is temporal-origin too (the Ascension/Pentecost-adjacent case
       {!of_temporal} produces) and typically Class2, so it would already
       fall through this branch's [rank = Class4] test harmlessly on its
       own -- RG 91 has no IV-class vigil at all (this file's own entry-27/
       28 comments), so [is_vigil && rank = Class4] should never occur on
       real data. Kept as an explicit, defensive guard (not load-bearing
       for real data, but total over every candidate {!Precedence.resolve}
       or {!Calendar} can construct, including shapes RG 91's table itself
       does not describe) rather than relying on that absence silently: a
       vigil, per RG 31 (II class, "si impediuntur, commemorantur") / RG 32
       (III class, "si impeditur, commemoratur"), is ALWAYS commemorated
       once RG 33 does not omit it outright, regardless of ferial class --
       a rule Caput IV does not speak to at all (vigils are Caput V, RG
       28-34, not "feriae"). RG 32's own full sentence, primary-source-
       verified (final fix wave): "Vigilia III classis est vigilia S.
       Laurentii. Haec vigilia praefertur diebus liturgicis IV classis; et,
       si impeditur, commemoratur, iuxta rubricas" -- confirmed word for
       word against the scan, not constructed by analogy with RG 31 (the
       register's own §4 "Vigils" entry states RG 32 only as "same pattern
       [as RG 31]", not verbatim -- now closed here). *)
    Precedence.Omit
  else if
    is_temporal
    && cel.Celebration.rank = Class2
    && is_sunday_slug (Slug.to_string cel.Celebration.slug)
    && (let wcel = winner.Precedence.cel in
        (wcel.Celebration.rank = Class1 || wcel.Celebration.rank = Class2)
        && wcel.Celebration.subject = Subject.Lord)
  then
    (* RG 16(a) (docs/research/rules-register.md §6.0, Caput III "De
       Dominicis", primary text): "Dominica II classis, in occurrentia,
       festis II classis praefertur. Attamen: a) festum Domini I aut II
       classis, in dominica II classis occurrens, locum tenet ipsius
       dominicae cum omnibus iuribus et privilegiis: de dominica, proinde,
       NULLA FIT COMMEMORATIO" -- a Feast of the Lord, I or II class,
       occurring on a II-class Sunday, takes the Sunday's own place with all
       its rights and privileges: OF THE SUNDAY, THEREFORE, NO
       COMMEMORATION IS MADE. {!band} entry 14 already ranks such a feast
       above the Sunday (RG 91 entry 14 < entry 15), so the feast is
       correctly [observed]; this is [disposition]'s own answer for what
       becomes of the SUNDAY once it has lost -- [Omit], not the [Commemorate]
       every other impeded II-class Sunday gets via RG 109(a)/RG 111(b)
       below. This is the ONE place in this function that needs the WINNER's
       [subject], not only the loser's own fields -- like RG 33's vigil
       omission above, not a new kind of signature: [Precedence.rules.disposition]
       already takes [~winner], this is simply its first other reader.

       Both winner-side conjuncts are load-bearing, proved by
       [test_precedence_ef.ml]'s own paired rows (the brief's "one without
       the other proves nothing"):
       - [rank = Class1 || rank = Class2]: {!band} entries 11-13 admit ANY
         I-class feast -- Lord or Saint alike, no subject test at all (entry
         14's own comment: unlike entry 16, entries 11-13 draw no such
         line) -- ahead of a II-class Sunday. REACHABLE at Class1 on real
         data: e.g. the Nativity of St John the Baptist (24 June, I class,
         Saint) landing on a Time-after-Pentecost Sunday already wins the
         day under {!band} alone; without this conjunct RG 16(a) would
         wrongly fire for it too. (At Class2 this conjunct adds no further
         cases beyond what the [subject = Lord] conjunct below already
         requires -- entry 16's ordinary II-class feasts never outrank a
         Sunday in {!band} in the first place, 16 > 15 -- so it is only
         independently reachable at Class1.)
       - [subject = Lord]: at Class2 this is what {!band} entry 14 already
         requires of its own winners, so it adds no further restriction
         there; at Class1 it is independently reachable and necessary, per
         the St John Baptist example above -- dropping it would fire RG
         16(a) for any winning I-class feast at all, exactly the
         over-wide branch the task brief warns against.

       The SUNDAY-side conjuncts: [rank = Class2] excludes a I-class Sunday
       (Advent/Lent/Passiontide, Low Sunday, {!band} entry 6) -- RG 16(a)'s
       own text says "Dominica II classis", not "any Sunday", and a I-class
       Sunday can never actually reach here as a [loser] against a
       Class1-or-2 Lord winner in the first place, for TWO DIFFERENT reasons
       depending on the winner's shape (CORRECTED, fix round 1: the previous
       version of this comment claimed "entry 6's own band value (6) is
       lower than every entry [3, 11-14]" as a single numeric argument --
       WRONG on its face, 6 is not lower than 3, and the claim would in any
       case prove the opposite of what it was cited for: if a band-3
       candidate really did contest a Class1 Sunday, the LOWER number (3)
       would win, i.e. the Sunday would LOSE, not "always win outright" as
       claimed):
       - Against entries 11-14 (SANCTORAL-origin Lord feasts, e.g. the
         Transfiguration): the numeric argument genuinely holds here --
         entry 6 (6) IS lower than 11-14, so a I-class Sunday always wins
         outright against these.
       - Against entry 3 (TEMPORAL-origin Lord feasts -- Epiphany,
         Ascension, Trinity, Corpus Christi, Sacred Heart, Christ the King):
         not a numeric argument at all, but a STRUCTURAL one -- every
         band-3 celebration is [is_temporal], and {!Precedence.resolve}
         takes exactly ONE temporal candidate per day ([~temporal], not a
         list), so a band-3 Lord feast IS that date's own single temporal
         candidate, never a SEPARATE candidate contesting an
         independently-produced Sunday on the same date. There is no
         collision to resolve by band comparison in the first place.
       Either way, no I-class Sunday can reach here as a loser against a
       Class1-or-2 Lord winner -- this is exactly why the rubric restricts
       itself to II class. [is_sunday_slug]
       (the same marker RG 33's [impedes_vigil] and RG 109(a)'s
       [privilege_of] already use, with no [is_temporal] guard there either
       -- no sanctoral slug this codebase's data produces contains
       {!sunday_marker}) is the "Dominica" half; [is_temporal] is kept
       alongside it anyway, the same explicit-but-not-load-bearing defence
       the RG26 branch above gives its own [not is_vigil] guard, rather than
       relying on that absence silently. *)
    Precedence.Omit
  else if winner.Precedence.cel.Celebration.subject = Subject.Lord && cel.Celebration.subject = Subject.Lord
  then
    (* RG 112(a) (docs/research/rules-register.md §4/§6.0, primary text,
       verified against BOTH photographic scans -- missale-romanum-1962.pdf
       and "Missale Romanum 1962_text.pdf" -- the electronic transcription,
       1962-06-23,_SS_Ioannes_XXIII,_Missale_Romanum,_LT.pdf, carries
       NEITHER this paragraph's own worked example NOR its own more
       specific Mass-proper rubric below, exactly the transcription's
       documented defect, this project's own methodology note): "112. Ad
       commemorationes et orationes quod attinet, haec insuper serventur:
       a) Officium, Missa aut commemoratio de aliquo festo vel mysterio
       UNIUS DIVINAE PERSONAE excludit commemorationem aut orationem de
       alio festo vel mysterio EIUSDEM DIVINAE PERSONAE" -- the Office,
       Mass, or commemoration of some feast or MYSTERY of ONE Divine Person
       excludes a commemoration or oration of ANOTHER feast or mystery of
       the SAME Divine Person.

       {!Subject.t} has no finer split within [Lord] than "a mystery
       touching the Divine Person of the Son" -- every real [subject =
       Lord] entry this codebase's own data carries concerns Christ
       specifically (register §6.0's subject audit: the Precious Blood, the
       Transfiguration, the Exaltation of the Cross, the Dedication of the
       Archbasilica, the Purification, the Baptism, and now Holy Family),
       so reading "same Divine Person" as simply "both [Lord]" is safe at
       today's data's own granularity -- a rite that ever needed to
       distinguish, say, a Trinity-specific mystery from a Son-specific one
       by Person would need a finer [Subject.t] first, not a special case
       added here.

       The one live witness -- confirmed DIRECTLY, not only by this general
       paragraph: the Holy Family Mass propers' own note, immediately
       following the Postcommunion, word for word on 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" -- if the feast of the
       Holy Family occurs on 13 January, the Mass is said of the Holy
       Family, WITHOUT commemoration of the Baptism of Our Lord Jesus
       Christ, and without commemoration of the Sunday (RG 17(b)'s own
       general rule, restated for this specific date). Independently
       confirmed against missalemeum (register §6.0): title "The Holy
       Family: Jesus, Mary & Joseph", commemorations [], the Baptism
       listed only under "displaced", never commemorated.

       Checked after RG 16(a) above (so a genuine RG 16(a) Sunday-
       suppression is never re-explained under this citation instead) and
       before the final ordinary-commemoration catch-all below, since
       without it a [Feast]-status loser of this shape would otherwise
       reach RG 95's ordinary "commemorated or omitted" branch and be
       admitted as an ordinary Class2 commemoration under RG 111(b) --
       confirmed wrong against both primary sources above. *)
    Precedence.Omit
  else
    (* RG 95's other branch: "aut commemorantur aut penitus omittuntur" --
       commemorated or wholly omitted. Reached by every SANCTORAL loser
       below I class (RG 111(c)/(d)'s "ordinary" commemoration, no closed
       list the way the temporal branch above has), AND by an impeded
       I-class Sunday (excluded from the [Transfer] branch above, and from
       the temporal Class4 [Omit] branch above because a Sunday is never
       IV class -- RG 11-12/91 entry 6/15 make every Sunday I or II class,
       never a "feria" at all in Caput IV's own sense, RG 21): RG 109(a)
       (§4) lists "of a Sunday" as a privileged commemoration
       category, which presupposes an impeded Sunday stays put rather than
       moving to another day the way a feast does -- [privilege_of] tags it
       [Privileged] via the same [is_sunday_slug] marker, with no further
       code needed here. Which of commemorate/omit survives is RG 108-111's
       admission count ([admit], below), not this function's decision; this
       only opens the commemoration, tagged with its real RG 109 privilege
       via [privilege_of].

       RG 94 (a fixed-day commemoration is not carried along with a
       transferred feast) needs no code here: [Precedence.resolve] calls
       this function once per loser, always against the day's actual
       [observed] winner -- never against a fellow loser that itself
       transferred away -- so no mechanism exists by which a commemoration
       could ride along with a departing feast in the first place; there is
       nothing to suppress. *)
    Precedence.Commemorate (privilege_of loser)

(* Task 9: how many of the day's commemorations RG 111 admits, and which
   (docs/research/rules-register.md §4, "Commemorations",
   RG 111). [band] decides who wins the day; [disposition] decides who is
   even eligible to be commemorated, and tags each with its RG 109 privilege
   via [privilege_of]; this decides how many of THOSE survive.

   RG 111 keys its four admission rules off the CLASS OF THE DAY ("diebus I
   classis", "dominicis II classis", "aliis diebus II classis", "diebus III
   et IV classis") -- read here off [observed]'s own [rank] and, for the
   Sunday/non-Sunday II-class split, the same slug marker [privilege_of] and
   RG 33's [impedes_vigil] already use ([is_sunday_slug]).

   CORRECTED (fix round 1, RG16(a) task): the previous version of this
   comment claimed "[observed] IS the day's own celebration, so its rank and
   slug already carry everything RG 111's own four categories test" --
   WRONG once RG 16(a) exists. RG 16(a)'s own text says the winning Feast of
   the Lord holds the Sunday's place "cum omnibus iuribus et privilegiis"
   (with ALL its rights and privileges) -- the day remains a "dominica II
   classis" for RG 111(b)'s own purposes even though [observed] is now the
   FEAST, not the Sunday, so [observed]'s own slug is no longer a reliable
   signal of "is this a Sunday" once something can legitimately observe in
   the Sunday's place. Confirmed wrong for real data by the oracle: 6 August
   falling on a Sunday shows the Transfiguration observed and NO
   commemoration (missalemeum: `commemorations: []`, Pope Sixtus II et al.
   `displaced`); the pre-fix code, reading Sunday-ness off [observed]'s own
   slug ("transfiguration-of-our-lord", no Sunday marker), wrongly took the
   [Class2, false] "other II class: one" branch below and admitted Sixtus
   (Class3) regardless of RG 111(b)'s own "de festo II classis" rank floor.
   Control, also oracle-confirmed: 6 August on an ordinary WEEKDAY (no
   Sunday collision) correctly admits Sixtus -- being a Sunday is exactly
   what excludes him, and [observed]'s own identity cannot tell the two
   cases apart on its own.

   Fixed by reading Sunday-ness off [temporal] instead -- {!Precedence.rules.admit}'s
   own [~temporal] parameter, {!Precedence.resolve}'s [~temporal] argument
   passed straight through, unaffected by whether it won the day. [context]
   (date/season/weekday) is still not needed: [temporal]'s own slug already
   carries everything this split needs, the same way [observed]'s used to
   before a competing office could occupy the Sunday's place. *)

(* RG 113 -- CORRECTED, Task B (branch ef-rg16a): docs/research/rules-
   register.md §4's RG 113 entry previously quoted only its FIRST sentence
   ("commemoratio de Tempore fit primo loco"); its SECOND, load-bearing
   sentence, primary-source-verified against two independent scans, is the
   real rule for this function: *"In admittendis et ordinandis aliis
   commemorationibus, servetur ordo tabellae praecedentiae"* -- in ADMITTING
   and ORDERING the other commemorations, the order of the table of
   precedence (RG 91, {!band}'s own 28-entry table) is to be kept.

   This REPLACES a previous [compare_dignity], which sorted by RG 8's coarse
   four-class [rank] ("dignity") and broke same-rank ties alphabetically by
   slug -- an engineering convention with no rubrical warrant. Measured,
   fix round 1 (2005-2050, a temporary pre-fix [git worktree]): a
   same-[rank] tie existed on 599 days, most never reaching a real
   decision; reversing the slug tie-break alone changed the ADMITTED SET on
   65 of them and the printed ORDER of an already-admitted pair on a
   further 149 (docs/research/rules-register.md §6.1's full account,
   correcting this comment's own earlier, unreproduced "66 days" claim).

   CORRECTED, fix round 1 (coordinator finding 1): checking every one of
   those 65+149 real decisions against data/ef/sanctoral.sexp found the
   SAME underlying shape in all of them, no exceptions -- one candidate is
   always [Cel.Commemoration_only] (e.g. "maurice-and-companions-martyrs",
   22 September) and the other always a genuine [Cel.Feast] or a temporal
   office of the SAME [rank] (e.g. "thomas-of-villanova", same day; or a
   Lent feria, {!band} entry 22, against "paul", 22 February). That is
   {!band}'s OWN fidelity bug, fixed separately at its source (see [band]'s
   own top-of-branch guard, RG 91's table has no row for a bare
   commemoration at all) -- NOT a case RG 113's table-order alone resolves,
   since a [Commemoration_only] candidate never had a real table entry to
   compare in the first place. With that fixed, none of these 599 days'
   real decisions any longer depend on THIS function's own slug fallback:
   {!band} alone (a real entry vs {!unclassified}) already decides every
   one. What remains genuinely open -- two DIFFERENT candidates landing on
   the IDENTICAL real table entry, e.g. two different Class3 universal
   feasts both at entry 24 -- was checked and found EMPTY across
   2005-2050 (register §6.1): RG 113's own table-order, once {!band} is
   accurate, already decides every real case this codebase's current data
   produces; the slug fallback below is exercised only between two
   [Commemoration_only] candidates tied at {!unclassified} (138 of the 599,
   all order/count-invisible -- RG 91 has no table position for either of
   them to compare, so there is nothing more specific RG 113 could supply
   here either). {!band} needs a [context] this function itself does not
   have (date/season/weekday) -- unlike [dignity], which read [rank] alone
   -- so {!Precedence.resolve} now computes each candidate's own [band]
   value once, generically, and hands it to [admit] as the trailing [int]
   on each input triple (see {!Precedence.rules.admit}'s own doc). [comms]
   below is [(candidate * privilege * int) list], not the pair it used to
   be. *)
let compare_precedence (a, _, ba) (b, _, bb) =
  if ba <> bb then Int.compare (ba : int) bb
  else Slug.compare a.Precedence.cel.Celebration.slug b.Precedence.cel.Celebration.slug

let rec take n = function
  | [] -> []
  | x :: xs -> if n <= 0 then [] else x :: take (n - 1) xs

(* [comms]'s own candidate/privilege pair, its [band] value dropped once a
   selection has been made -- {!Precedence.rules.admit}'s return type is
   still the pair, not the triple; only the INPUT carries [band]. *)
let drop_band (c, p, (_ : int)) = (c, p)

let admit ~(observed : Vocab_ef.rank Precedence.candidate)
    ~(temporal : Vocab_ef.rank Precedence.candidate)
    (comms : (Vocab_ef.rank Precedence.candidate * Precedence.privilege * int) list) :
    (Vocab_ef.rank Precedence.candidate * Precedence.privilege) list =
  (* Sorted once, by {!band} then slug (see [compare_precedence]); every
     branch below either takes a prefix of this list or filters it, so the
     RESULT is always built from [comms]'s own elements, untouched -- never
     rebuilt -- which matters beyond determinism: {!Precedence.resolve}'s
     own [dropped] computation tells an admitted candidate from a dropped
     one by physical equality (==) on the candidate value (Task 2's own
     deferred note: "assuming admit returns the same candidate values rather
     than rebuilt ones; undocumented for rite authors" -- documented here,
     now that this is the function that note was about). Building a fresh
     [{ c with ... }] record anywhere below would silently defeat that
     accounting: the original would then match nothing in [admitted], so the
     celebration would surface TWICE in the same day -- once in
     [commemorations] (the rebuilt copy) and once in [omitted] (the original,
     which nothing admitted matches). One admission, double-reported, and no
     crash to announce it, which is exactly why this comment exists.
     [drop_band] only unwraps the pair back out of the triple -- it does not
     rebuild [c] or [p] themselves, so this obligation still holds. *)
  let sorted = List.stable_sort compare_precedence comms in
  let is_privileged (_, p, _) = p = Precedence.Privileged in
  let observed_rank = observed.Precedence.cel.Celebration.rank in
  (* CORRECTED (fix round 1, RG16(a) task): read off [temporal], not
     [observed] -- see this function's own doc comment above for the full
     argument and the oracle evidence. [temporal] is the day's own
     temporal-cycle candidate regardless of who is [observed]; RG 16(a)
     makes that distinction live for the first time (a Feast of the Lord
     can now be [observed] on a day whose [temporal] candidate is a Sunday). *)
  let day_is_sunday =
    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 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 ->
      (* 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"
         entry previously carried only the fragment "de festo II classis",
         not the full sentence -- now added there too): "in dominicis II
         classis, una tantum admittitur commemoratio, SCILICET DE FESTO II
         CLASSIS, quæ tamen omittitur si commemoratio privilegiata facienda
         sit" -- "on Sundays of the II class, only ONE commemoration is
         admitted, NAMELY OF A FEAST OF THE II CLASS, which however is
         dropped if a privileged commemoration is due." Two clauses, not
         one: (i) a privileged
         commemoration, whenever due, categorically takes the day's one slot
         -- not by comparing its precedence against the ordinary contender's,
         so an ordinary commemoration that would otherwise win on raw
         table order is still dropped once any privileged one is also due
         (the asymmetric clause the brief and task report flag as
         deliberate, not present at "other II class" below); (ii) failing
         that, the slot is reserved SPECIFICALLY for a [Class2] candidate --
         "de festo II classis" is a RANK restriction, not merely "whichever
         ordinary candidate has the best table position": a III- or
         IV-class ordinary loser (a plain commemoration-only saint with no
         privilege of its own) has NO standing for this slot at all and
         must be entirely omitted, even when it is the only candidate
         present.

         Fix, Task 16 (primary-source-verified + missalemeum-confirmed):
         previously this fell back to "the best of [sorted], whatever its
         rank" once no privileged candidate was due, silently admitting a
         III/IV-class ordinary saint that RG 111(b)'s own wording excludes.
         Confirmed wrong for real data by the oracle comparison: e.g. 11 Jan
         2026 (Holy Family, a II-class Sunday) has St Hyginus (Class3,
         commemoration-only) as its only competing candidate -- missalemeum
         shows him "displaced" (omitted), never commemorated; the
         pre-fix code admitted him regardless. *)
      (match List.filter is_privileged sorted with
       | best :: _ -> [ drop_band best ]
       | [] -> (
           match List.filter (fun (c, _, _) -> c.Precedence.cel.Celebration.rank = Class2) sorted with
           | [] -> []
           | best :: _ -> [ drop_band best ]))
  | 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)

(* Task 11: RG 96 -- where an impeded I-class feast lands (docs/research/
   rules-register.md §4, "Transfer/translation"). [band] decides who is
   impeded; [disposition] decides that an impeded I-class FEAST (not a
   Sunday, not omitted by RG 33) is [Transfer]-disposed; this is the third
   and final question RG 96 poses -- WHERE the translation lands -- and is
   {!Rite.t.transfer_target} itself, called by {!Calendar}'s placement pass
   once per deferred candidate, never re-run once a target is accepted
   (calendar.ml's own comment on [~start ~stop]).

   RG 96's own text, register-transcribed: "the next following day that is
   not I or II class." [is_blocking] reads that off [Vocab_ef.rank] --
   RG 96 speaks of the day's CLASS (RG 8's four-way "dignity"), not [band]'s
   finer 28-entry occurrence-table row -- unlike {!admit} above, which (RG
   113, Task B/ef-rg16a) now DOES use [band] itself for its own selection
   order; RG 96's own text has no such finer-table reading, so [is_blocking]
   stays on [Vocab_ef.rank] alone. *)
let is_blocking (rank : Vocab_ef.rank) = rank = Vocab_ef.Class1 || rank = Vocab_ef.Class2

(* RG 96's own named exception (docs/research/rules-register.md §4,
   "Transfer/translation", RG 96 Attamen (a) -- primary-source-verified
   2026-08-12, corrected from an earlier unconditional transcription; see
   the register's own correction note). Verbatim: "festum Annuntiationis
   B. Mariae Virg., quando est transferendum post Pascha, transfertur,
   tamquam in sedem propriam, in feriam II post dominicam in albis" -- when
   [the feast] is to be transferred PAST EASTER, [it] is transferred, as to
   its own proper seat, to the Monday after Low Sunday. The exception is
   CONDITIONAL on that "past Easter" clause -- {!transfer_target} tests it
   by comparing the GENERAL RG 96 target against Easter itself, not by
   testing the date here. Identified by slug -- the same convention this
   file already uses to pick out one specific celebration from a rank/
   status shape shared by many others ({!nativity_octave_prefix},
   [is_ember_18]'s date anchors) -- not an RG citation itself: RG 96 does
   not encode how a computer recognises "the Annunciation", only what
   happens to it once recognised. data/ef/sanctoral.sexp's own bootstrapped
   slug (Task 10), reused verbatim rather than guessed. *)
let annunciation_slug = "annunciation-of-the-blessed-virgin-mary"

(* Not an RG citation -- a defensive engineering ceiling, the same role
   Calendar's own [max_transfer_rounds] plays for the OUTER round loop
   (calendar.ml). That guard bounds how many ROUNDS the whole-year placement
   pass takes; it does nothing for the walk a single call to this function
   makes internally, which is this module's own responsibility (rite.mli
   documents the obligation this constant exists to satisfy). Comfortably
   longer than the longest real run of consecutive I/II-class days the 1962
   calendar produces -- 24 Dec to 1 Jan (the Nativity vigil through the
   Circumcision, both I class, with the intervening octave days II class) is
   9 days; Easter through Low Sunday (the Easter octave, I class, entry 10)
   is 8 -- RG 91 entry 28's own unqualified IV-class catch-all guarantees a
   non-blocking feria follows any such run in real data. Not tuned to that
   bound any more than 64 is tuned to RG 97-98's real collision count: a
   ceiling nothing in the 1962 calendar comes close to, so a rite/data shape
   this module has not anticipated fails FINITELY (see [search_from]) rather
   than hanging the CLI. *)
let max_search_days = 400

(* The domain's own ceiling ({!Date.make}'s documented 1583..9999 bound,
   also duplicated by calendar.ml's own [domain_max_date] for the same
   reason: neither module exposes it to the other, and this is a three-line
   constant, not worth a new signature just to share it). [search_from]
   below must never call [occupant] on a date past this: [occupant] chains
   through the rite's own [temporal] (calendar.ml's [resolve_with_injected]),
   which for the real EF rite calls [Computus.gregorian_easter], which is
   NOT total outside 1583..9999 -- it constructs a [Date.t] via [Date.make]
   and [failwith]s on [Error]. [Date.add_days] itself has no such limit (it
   is documented "unbounded total arithmetic"), so [search_from] CAN walk
   [d] past 31 December 9999 without raising by itself -- the raise would
   only happen on the NEXT [occupant d] call, which is exactly the bug this
   guards against: an I-class feast impeded late enough in civil year 9999
   that every remaining day of the year is also I or II class (reachable
   through the project's own overlay mechanism, confirmed by review: an
   Add-ed I-class feast on 25 December leaves only Class2 Nativity-octave
   days for the rest of 9999, so the unguarded walk reached 1 January 10000
   and crashed there). *)
let domain_max_date =
  match Date.make ~year:9999 ~month:12 ~day:31 with Ok d -> d | Error e -> failwith e

(* Walks forward from [d], returning the first date [occupant] reports as
   NOT [is_blocking]. [steps] is a strictly increasing structural bound on
   the recursion, capped at [max_search_days]: the function decreases
   [max_search_days - steps] by exactly one on every call and returns as
   soon as that reaches zero (whether or not an admissible day was ever
   found), so THIS loop terminates by construction, regardless of what
   [occupant] reports -- it does not rely on the real EF calendar's own
   structure to guarantee termination the way the comment above explains
   why the bound is never actually reached in practice. Also stops, without
   calling [occupant] again, once [d] passes {!domain_max_date} -- see that
   constant's own comment for why probing [occupant] beyond it can raise.
   Either way the last date visited is returned WITHOUT a further
   [occupant] probe -- one more finite (not necessarily admissible) date,
   not a further search -- because the value the caller ([transfer_target])
   is still owed is "a date", never an exception; {!Calendar}'s own
   [~start ~stop] bound (calendar.ml's [place_transfers]) is what turns an
   implausible non-terminating real search into a recorded [omitted], not
   this function pretending to have found something admissible. *)
let rec search_from (occupant : Date.t -> Vocab_ef.rank Celebration.t) (steps : int) (d : Date.t) :
    Date.t =
  if steps >= max_search_days || Date.compare d domain_max_date > 0 then d
  else if is_blocking (occupant d).Celebration.rank then search_from occupant (steps + 1) (Date.add_days d 1)
  else d

(* [transfer_target]'s contract (rite.mli): total, terminating, and its
   result is always strictly after [origin]. Terminating: [search_from]'s
   own structural bound, above. Strictly after [origin]: the general branch
   is exactly [search_from]'s own result starting at [Date.add_days origin
   1], which only ever advances forward from there, so it is always >=
   origin + 1. The Annunciation branch, when it fires, instead searches from
   the Monday after Low Sunday for [origin]'s own civil year -- NOT provably
   later than [origin] by the code alone, but true of every representable
   year: the Annunciation's [origin] is always 25 March (Date_spec.Fixed in
   data/ef/sanctoral.sexp), Easter always falls within that SAME civil year
   in [22 March, 25 April] (Computus's own documented range, register §0),
   so Low Sunday (Easter + 7) falls in [29 March, 2 May] and the Monday
   after it in [30 March, 3 May] -- always after 25 March.

   RG 96 Attamen (a) (see {!annunciation_slug}'s own comment) makes the
   Annunciation exception CONDITIONAL on the general walk carrying the
   feast past Easter -- so the general target is always computed FIRST,
   for every candidate, and only overridden for the Annunciation when that
   target itself falls after Easter Sunday. A version of this function that
   tested the DATE of [origin] instead (e.g. "is 25 March within some fixed
   window of Easter") would be re-deriving the register's own "quando est
   transferendum post Pascha" condition from first principles, exactly the
   kind of guess this project's "a wrong citation is worse than a missing
   one" rule warns against; comparing the general target against Easter
   directly tests the rubric's own words. *)
let transfer_target (c : Vocab_ef.rank Precedence.candidate) (origin : Date.t)
    (occupant : Date.t -> Vocab_ef.rank Celebration.t) : Date.t =
  let general_target = search_from occupant 0 (Date.add_days origin 1) in
  let is_annunciation = Slug.to_string c.Precedence.cel.Celebration.slug = annunciation_slug in
  let easter = Computus.gregorian_easter (Date.year origin) in
  if is_annunciation && Date.compare general_target easter > 0 then
    (* Low Sunday = Easter + 7 (register §0, temporal_ef.ml's [off 7]); the
       Monday after it = Easter + 8. Searched onward from there exactly
       like the general case searches from [origin + 1] -- "only if that
       day is itself blocked" (rite.mli) is [search_from]'s ordinary
       behaviour, not a second mechanism. *)
    search_from occupant 0 (Date.add_days easter 8)
  else general_target